Skip to content

osfameron/tramp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tramp: threading macro with continuations

Tramp has a single macro tramp-> which lets you write a thread like ->, but also ->> or some-> if you need it, as well as resumable continuations.

(require '[tramp :refer [tramp->]])

(tramp-> 1 inc inc)
;; => 3

Customizable ordering

If you don't want thread-first, you can specify the position of the threaded argument with %:

(tramp-> 3
         range
         (map inc %))
;; => (1 2 3)

Guards!

You might use some-> to create a thread which terminates on seeing any nil value. But sometimes it's hard to tell which forms in the thread you're expecting to return a nil. And sometimes you'd like to use a different measure of success.

(tramp-> id
         lookup-in-db
         (guard some?)
         parent-id
         (guard some?))

(tramp-> num
         inc
         (guard odd?)
         (str "The answer was " %)))

You can also return a thread early with:

(tramp-> num
         inc
         (return 5) <-- rest of this thread will never be called
         inc
         inc)

This might be useful for debugging, or in conjunction with if->:

If

The if-> macro takes a predicate, a true branch and an optional false branch. These branches are themselves tramp-> threads.

If no false branch is provided, the value is passed through unchanged. If you wanted the else branch to return nil, you might look at guard or at return'ing a value.

(tramp-> i
         (if-> odd?
            ((inc)
             (* 2))
            ((* 3)
             (dec)))
         (str "The answer is: " %))

; always return an even number.
(tramp-> i
         (if-> odd?
            (inc)))

; return out of the outer thread
(tramp-> i
         (if-> odd?
            ((inc)
             (* 2))
            (return nil)
         (str "The answer is: " %))

Break on !

(defn myfunc [i]
   (tramp-> i
            (inc)
            ! (inc)
            (str)))
;; #'boot.user/myfunc

Because this function call has an ! in it before the second (inc) form, it will break on that when called, returning a function:

(myfunc 1)
;; #object[clojure.lang.AFunction$...]

... and that function returns the answer!

((myfunc 1))
;; "3"

Obviously calling functions like ((((f)))) all the time would be horrendous! Luckily Clojure has a builtin function that recursively calls a function until the final result is returned: trampoline.

So you could call the whole thing with:

(trampoline myfunc 1)
;; "3"

But the intermediate functions are annotated with metadata. So you could also stop to check that the intermediate steps are correct...

(:fn (meta (myfunc 1)))
;; #object[clojure.core$inc ...]

(:args (meta (myfunc 1)))
;; [2]

You can also override the value your function should have returned: (e.g. here we pretend that (inc 2) => 10)

((myfunc 1) 10)
;; "10"

; or, if you prefer using the helper `step!`:

```clojure
(-> (myfunc 1) (step! 10))
;; "10"

Why?

This makes it possible to test functions that mix pure (core) logic and effectful (shell) interactions without mocking.

Unlike mocks, the logic that is actually exercised is actually the code that will be run in live. The only thing that you need to override is the values that the effectful functions would have returned.

For example:

(defn get-parent-item [item]
   (tramp-> item
            :parent-id
            core/prepare-db-get-request
            !(effect/db-get-request)
            :item))

We can't easily test that (effect/db-get-request) will return the right value, but we know what it should return given that input.

So we could test it like this (using some additional helper macros which call clojure.test/is:

(deftest test-get-parent-item
   (testing "look Ma, no with-redefs!"
      (-> (get-parent-item {:id "ID" :parent-id "PARENT"})
          (is-fn effect/db-get-request)
          (is-arg {:table "foo" :id "PARENT"})
          (step! {:item {:id "PARENT"}})]
          (is-result {:id "PARENT"})))

About

Threading macro that returns continuation

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published