Clojure has a couple of macros for threading a value through a bunch of computations, -> and ->>. The syntactic benefit added by these macros can be seen in examples here and here.

These macros are definitely useful, but being macros, they deal with syntactic forms instead of functions, and thus make a bunch of things harder than they need to be. Some such cases:

  1. Running a computation conditionally.
  2. Nil-shortcutting. This is a special case of item 1, where the condition is that the element is not nil.
  3. There are two macros for dealing with two common argument positions, first and last. What happens when the argument has to go somewhere else? Or when, in a single pipeline, you need to invoke two computations with two different argument positions?
  4. Dropping a real function value into the pipeline.
  5. Threading a value through a bunch of iteratively obtained computations.
  6. Tapping an immediate value and passing it forward.
  7. Getting hold of intermediate results.

These and many similar problems have spawned a small cottage industry of people developing macro extensions or macro alternatives to the two standard threading macros. Here is how some of those solutions try to address the problems mentioned above:

  1. Prismatic developed penguin operators, ?> and ?>>. Pellet came up with if->, when->, and Chris Houser’s Synthread has when. Clojure core later added cond-> and cond->> to address the same problem. All of these are special-purpose macros which transform the given expression into a form acceptable to -> and ->>.
  2. Swiss Arrows has some-<> and some-<>>. A variation of the same has made it to the standard library under the name some->, and its cousin some->>.
  3. Swiss Arrows’ -<> and -<>> allow you to specify the argument position explicitly. I find this better. Synthread’s as, Pellet’s arg->, and now core’s as-> allow you to name intermediate results, which can also help in marking the argument position.
  4. In the current implementation, people typically just put another set of parentheses around a function value, for example (-> 5 ((fn [x] (- x 7)))). It works, but feels hacky. Prismatic’s plumbing library has a fn-> macro which makes this a little nicer syntactically.
  5. Pellet’s for->.
  6. Synthread’s aside.
  7. See item 3.

There is a common theme in all of the above solutions:

  1. They are all macros. These are typically developed in one of two ways: either as a whole new set of macros with additional semantics, like Swiss Arrows, or as macros that expand their input forms into something acceptable to -> and ->>, and can thus be used with them. This does not strike me as especially elegant.
  2. Many of them come in two flavours, one that works with ->, and another that works with ->>.
  3. As evidenced by item 1 above, macros are the primary way used for extension. It is hard to extend this framework with functions.

Here I am proposing a dead-simple threading solution, purely based on functions and combinators.

(ns piper.core)
(defn pipe [value & fns]
(reduce (fn [acc cur] (cur acc)) value fns))
(defn given [pred f]
(fn [x]
(if (pred x)
(f x)
x)))
(defn unless-nil [f]
(fn [x]
(if (nil? x)
nil
(f x))))
(defn tap [msg]
(fn [x]
(do
(println (str msg ": " x))
x)))
(defn times [n f]
(fn [x]
(loop [value x
remaining-turns n]
(if (zero? remaining-turns)
value
(recur (f value)
(dec remaining-turns))))))

Pretty simple. Here is some example use from the REPL:

user=> (require '[piper.core :as pi])
nil
user=>
user=> (pi/pipe {:a 3 :b 11}
#_=> (pi/given #(> (:a %) 2)
#_=> #(update-in % [:a] inc))
#_=> (pi/tap "i")
#_=> (pi/unless-nil #(assoc % :c 25))
#_=> (pi/tap "ii")
#_=> #(get % :a)
#_=> (pi/tap "iii")
#_=> (pi/times 6 inc)
#_=> (pi/tap "iv"))
i: {:a 4, :b 11}
ii: {:c 25, :a 4, :b 11}
iii: 4
iv: 10
10
user=>

Some benefits of this approach:

  1. It deals entirely with function values. The pipe combinator only cares that it is given a bunch of functions that can be called one after another. That is the only contract it has. It does not care how those function values came to life.
  2. Which means it is quite easy to extend this system with new combinators. As an exercise, try writing a combinator that executes the given function and on exception defaults to a fallback value.
  3. You do not need to invent a new convention or syntax for argument positioning. The user can simply use fn, #(...), partial, or whatever else seems appropriate, and benefit from syntax and functions that are already there.
  4. Getting hold of an intermediate value can be done in a straightforward way with nested pipes.

It is not all upside, though. There are some real downsides too:

  1. The most common use cases of -> and ->> would, when translated to this approach, perhaps lead to noisier code using either #(...) or partial.
  2. The macro-based approaches we talked about move forms around, insert arguments where needed, create intermediate let bindings, and avoid generating function values as much as possible. This leads to better performance.

This perhaps suggests that a function-based approach has occurred to people before, but that they may have set it aside for the reasons above.

Nevertheless, I may still try using this on some project, just to see how well it scales in terms of readability and performance.