(This post is based on a lot of the work presented in Greg Hendershott's blog post "The ~> Threading Macro", however this implementation does not use macros)

The thrush combinator combines other functions in such a way that it is the reverse of function composition. That's fairly abstract, so a better description would probably be to compare it to UNIX piping. In most Linux systems, there is a special command designated with the | symbol, which is used like this:

  commandA | commandB | commandC

What this means is basically "take commandA, run it, and send it's output as the input to commandB, then do the same for commandB to commandC." This is called "piping", because it can be thought of as joining together commands in a chain with each one joined to the next linearly, resulting in one large command that "pipes" it's input through commands.

This is what the thrush combinator does, with slightly different syntax. If implemented in Racket, (thrush 7 add1 even?) would take 7, add one to it to get 8, then check if 8 is even, so the entire expression would evaluate to true. Note that this is equivalent to (even? (add1 7)), which is simple function composition exemplified in Racket with ((compose f g) x). So the thrush combinator can be seen as applying reverse function composition:

  (define (thrush v . fs)
    ((apply compose (reverse fs)) v))

This allows us to write (thrush 7 add1 even?) and evaluate it, but thrush is a bit of a disconnected name. In the language Clojure, the thrush combinator is represented with the symbol ~> and is called "thread-first". The symbol makes enough sense to read that we'll use it in our definition:

  (define (~> v . fs)
    ((apply compose (reverse fs)) v))

Using this function in some contexts can be unwieldy however. Suppose we wanted a function that took a number, doubled it, and added seven to that. Defining the function is easy enough:

  (define (double-and-add-seven x)
    (~> x
        (λ (n) (* n 2))
        (λ (n) (+ n 7))))

The x argument is only used in one place and only serves as the input to the doubling function and the add seven function. This is even more of a pain if we wanted to create this function anonymously and pass it to some other function like map:

  (map
    (λ (x)
        (~> x
            (λ (n) (* n 2))
            (λ (n) (+ n 7))))
    (list 1 2 3 4 5))

This does the job and evaluates to (list 9 11 13 15 17) as expected, but writing that function is a bit of a pain compared to using compose:

  (map
    (compose
        (λ (x) (+ x 7))
        (λ (x) (* x 2)))
    (list 1 2 3 4 5))

This cuts down on some of the wordiness and removes that irritating outer lambda, but we lose the expressiveness of the thrush combinator as a chained sequence of operations in first-to-last order because compose would apply the functions in the wrong order if we gave the doubling function first and the add seven function second. We can however, create a version of the thrush combinator that instead of evaluating its value argument through its function arguments, takes only the functions and returns a function that, when applied to a value, evaluates the same as the normal thrush combinator. Or, in code:

  (define (λ~> . fs)
    (apply compose (reverse fs)))

With this definition of λ~>, evaluating (λ~> add1 even?) gives a function that when given an argument, adds 1 to it and checks if it's even. So we could solve our map problem as follows:

  (map
    (λ~>
        (λ (x) (* x 2))
        (λ (x) (+ x 7)))
    (list 1 2 3 4 5))

Which has the same logic of ~> but gets rid of the wrapping lambda. Additionally, this lets us redefine ~> in terms of λ~>:

  (define (~> v . fs)
    ((apply λ~> fs) v))

And now we have the ability to represent complex constructs as logical sequences of operations:

  (~> user-input
    get-pass
    access-database
    retrieve-user-data
    send-data)

Without this syntax, that code would have to be written:

  (send-data (retrieve-user-data (access-database (get-pass user-input))))

Which is difficult to read, and lacks an easy to grasp concept of the "flow" of values through the function. Our thrush combinator functions are easy to use and provide a good abstract representation of a computation as a sequence of chained steps.

To make this tool a little friendlier and more powerful, see Extensions to the Thrush Combinator.


1,417 0 0