The thrush combinator is very useful for allowing us to express a computation as a series of function calls in a declarative way, but it has some issues in the way of rigidity. It only allows simple functions of one argument to be passed to it. If you need more complex processing, even if it still fits into the model of "a series of steps", it requires some awkward anonymous functions to smooth the process along. For instance, consider writing a function that needs to:

  1. Take as input a list of numbers
  2. Filter out everything but the integers (no real numbers allowed)
  3. Filter out zero
  4. Split the integers into a list of positives and a list of negatives
  5. Square the positives
  6. Sum the negatives
  7. Subtract the negative-sum from each of the squared positives
  8. Return a sum of the final results from 7.

Despite the fact that this is a ridiculously contrived example, since each step retrieves its input directly from the step before it and sends its output directly to the step after it, it should be easy to represent this with the ~> function for the thrush combinator. Well, in a way it is, but it certainly looks rather ugly:

  (define (computation numbers)
    (~> numbers
        (λ (xs) (filter integer? xs))
        (λ (xs) (list (filter positive? xs) (filter negative? xs)))
        (λ (xs) (list (map sqr (first xs)) (second xs)))
        (λ (xs) (list (first xs) (apply + (second xs))))
        (λ (xs) (map (λ (x) (- x (second xs))) (first xs)))
        (λ (xs) (apply + xs))))

That is a lot of anonymous functions, and most of them are to do simple filtering, mapping, and applying operations. We can cut down on this with three simple utility functions:

  (define ((λfilter f) xs)
    (filter f xs))
(define ((λmap f) . ls)
    (apply map (cons f ls)))
(define ((λapply f) xs)
    (apply f xs))

What we're doing here is taking advantage of a nifty shorthand in Racket. When you define a function, normally you do so like (define (f arg1 arg2) expressions ...). But when you put ((f arg1) arg2) instead of (f arg1 arg2), you're defining a function that behaves in a different manner. In particular, that version of f first accepts only one argument, binds it to arg1, then returns another function that also accepts only one argument. When that function is called, it uses its argument as arg2 and then evaluates the expressions ... defined for it. So with our version of filter using this tool, defined as λfilter, we can now write:

  (define filter-even? (λfilter even?))
(displayln (filter-even? (range 10)))
(displayln (filter-even? (range 20)))

We can use λfilter as a "meta-function", we give it what we want to filter by and it creates a new function that filters lists according to the predicate provided. λmap behaves similarly, but with a little trickery to cope with the fact that map can accept a variable number of lists. To cope with that, we first construct the full argument list by cons-ing the function provided, f, onto the list of argument lists, ls, to get a list that looks like (f l1 l2 l3 ...), then we apply map to this argument list. So now we've got a function λmap, as well as a similarly-defined function λapply, that both behave like λfilter. Using these, we can rewrite our earlier definition:

  (define (computation numbers)
    (~> numbers
        (λfilter integer?) ; only accept integers
        (λ (xs) (list (filter positive? xs) (filter negative? xs)))
        (λ (xs) (list (map sqr (first xs)) (second xs)))
        (λ (xs) (list (first xs) (apply + (second xs))))
        (λ (xs) (map (λ (x) (- x (second xs))) (first xs)))
        (λapply +)))

This didn't help as much as we'd like however. The problem is that in the middle portion of the computation, we're not operating on one list of numbers anymore - we're operating on two. So our map filter and apply logic needs a level of complexity added to cope with the indirection. This can also be fixed however, with the introduction of another handy tool which we'll call λlist:

  (define ((λlist . fs) xs)
    (cond [(empty? fs) xs]
          [(empty? xs) (error "List did not contain enough elements")]
          [else (list* ((first fs) (first xs))
                       ((apply λlist (rest fs))
                       (rest xs)))]))

What this function does is it takes a list of functions first. Then it returns a new function that accepts a list, and maps each function to an element in the list. So (λlist add1 sub1 sqr) returns a function that accepts a list of three items, adds one to the first, subtracts one from the second, and squares the third, then returns those results as a new list. Although the way this function is written, it accepts lists that are too long and simply returns those elements unchanged. This tool makes it very easy to define the function in our computation for taking a list of a list of positives and a list of negatives, squaring the positives, and summing the negatives:

  (λlist (λmap sqr) (λapply +))

That's it. Seriously. This function takes a list of two lists such as ((1 2 3 4 5) (-1 -2 -3 -4 -5)) and returns ((1 4 9 16 25) -15). With this very powerful tool we can now significantly improve our computation function:

  (define (computation numbers)
    (~> numbers
        (λfilter integer?)
        (λ (xs) (list (filter positive? xs) (filter negative? xs)))
        (λlist (λmap sqr) (λapply +))
        (λ (xs) (map (λ (x) (- x (second xs))) (first xs)))
        (λapply +)))

We've reduced the portion to square the positives and sum the negatives from two functions down to one function half the size of either of the two original functions. The most awkward lines remaining are the portion for splitting the list into positives and negatives, and for subtracting the negative sum from each of the squared positives. For that second line, we can eliminate the logic of taking the single input list and extracting the squared positives and the negative sum by making it a function of two arguments and wrapping it in λapply:

  (define (computation numbers)
    (~> numbers
        (λfilter integer?)
        (λ (xs) (list (filter positive? xs) (filter negative? xs)))
        (λlist (λmap sqr) (λapply +))
        (λapply (λ (xs y) (map (λ (x) (- x y)) xs))
        (λapply +)))

That's a touch friendlier, and lets us ignore the detail that it's essentially a function of two arguments whose arguments are being passed as a list. As for the line about filtering, we could create a utility filter-list that takes as many predicates as we want, and returns a list of lists where each list is the result of using filter with one of those predicates:

  (define (filter-list xs . ps)
    (map (λ (p) (filter p xs)) ps))

Note that because we're accepting a variable number of predicates, they have to come after the list of items to be filtered. This is somewhat awkward because it clashes with the argument ordering of filter, but we can ignore that problem by creating a λfilter-list form of the function that only accepts the predicates, then returns a function that accepts the list. This also plays nicely with the definition of the computation we've been working on. Specifically, λfilter-list is defined as:

  (define ((λfilter-list . ps) xs)
    (apply filter-list (cons xs ps)))

Now if we call (λfilter-list even? odd?) we'll get a function that takes a list of numbers, say (1 2 3 4 5 6), and returns a list of two lists, the first being the results of (filter even? (1 2 3 4 5 6)) and the second being the same but with odd?. So it would return ((2 4 6) (1 3 5)). Using this function, we can rewrite that awkward line we had in our computation:

  (define (computation numbers)
    (~> numbers
        (λfilter integer?)
        (λfilter-list positive? negative?)
        (λlist (λmap sqr) (λapply +))
        (λapply (λ (xs y) (map (λ (x) (- x y)) xs))
        (λapply +)))

This function performs the computation we outlined, and does it all by combining simpler functions with general function combinators. We never had to write a specific function for the task "split the list of numbers into positives and negatives", or "subtract the negative sum from all the positive squares", it was all achievable with function composition tools and anonymous functions. This is a large part of the power of functional programming, it allows us to abstract away "plumbing" operations to express the pure relationships of our program in a simple and declarative fashion.