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 should be. Some such cases:

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

These, and many such other problems, have spawned to a cottage industry of people developing macro extensions or macro alternatives to the two standard threading macros. Let’s see how some of those solutions try to address the problems mentioned above:

  1. Prismatic developed penguin operators, ?> and ?>>. Pellet came up with if->, when-> etc. Chris Houser’s Synthread has when. Clojure core recently added cond-> and cond->> to address the same problem. All of these are special purpose macros which transform the given expression in a form acceptable to -> and ->>.
  2. Swiss Arrows has some-<> , some-<>>. A variation of the same has made it to the standard library, under name some->. (And its cousin some->>.)
  3. Swiss Arrows’ -<> and -<>> (diamond wand and diamond spear respectively) allow you to explicitly specify the argument position. (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 parens around a function value. e.g. (-> 5 ((fn [x] (- x 7)))). (Yeah, hacky.) Prismatic’s plumbing library has a fn-> macro which makes this syntactically a bit nicer.
  5. Pellet’s for->.
  6. Synthread’s aside.
  7. See #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 a whole new set of macros with additional semantics, like Swiss Arrows. Or, macros that expand their input forms in a way acceptable to -> and ->>, and thus can be used with them. This doesn’t strike me as very elegant.
  2. Many of them come in two flavors - one that works with ->, another that works with ->>.
  3. As evidenced by #1, macros are the primary way used for extension. It’s hard to extend with functions in this framework.

Here I am proposing a dead simple threading solution, purely based on functions/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)

(defn unless-nil [f]
  (fn [x]
    (if (nil? x)
      (f x))))

(defn tap [msg]
  (fn [x]
      (println (str msg ": " x))

(defn times [n f]
  (fn [x]
    (loop [value x
           remaining-turns n]
      (if (zero? remaining-turns)
        (recur (f value)
               (dec remaining-turns))))))

Pretty simple, isn’t it? Here is some example use (from REPL):

user=> (require '[piper.core :as pi])

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

Some benefits of this approach:

  1. It deals entirely with function values. The pipe combinator only cares that it’s given a bunch of functions that can be called one after another. That’s the only contract it has. It doesn’t care how these function values came to life.
  2. …which means it’s quite easy to extend this system with new combinators. For an exercise, try writing a combinator that executes the given function and on exception defaults to a given fallback value.
  3. You do not need to invent a new convention or new syntax for the argument positioning. The user can simply use fn form, or #( reader macro, or partial, or whatever strikes their fancy, and benefit from the already present syntax/functions.
  4. Handle to an intermediate value can be done in a straightforward way with nested pipes.

It’s not all good and shiny though. There are some major downsides too:

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

This perhaps indicates that the function based approach may have occurred before to people, but they would have abandoned it for aforementioned reasons.

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