These Are Not the K Combinators You Are Looking For
Since version 1.9, Ruby has had Object#tap
. tap
started life as Rails
ActiveSupport Object#returning
. Here’s the old Rails source code:
Pass returning
an object and a block and it returns the object, after running the block on that object. Object#tap
is a slight variation on this theme[1].
Clojure has a similar function called doto
that generalizes the idea. Here’s an example:
The comment in the Ruby on Rails code and Jamis Buck’s, 2006 Mining ActiveSupport: Object#returning
refer to this functionality as an example of the K combinator. Though returning
and doto
are useful, and they can really improve your (object-oriented, side-effect-dependent) code by making your intentions plain, they are not examples of the K combinator.
Contrary to Buck’s analysis, a K combinator is not really a “function of two arguments”. Well, in a curried language like Haskell, it is usable as a function of two arguments. But the way that (potentially) two-argument function is almost always used, is as a one-argument function that produces a function. Dig Haskell’s const
, an actual K combinator:
Indeed you see what is potentially a two argument function. But the way const
will be used is:
See what happened there? const 42
produces a function, which, when passed any parameter, e.g. 0, 1, 2, or 3, will just return 42.
Similarly, Clojure has (constantly x)
that:
Returns a function that takes any number of arguments and returns x.
These are K combinators.
Object#returning
and doto
are certainly higher-order functions (doto
actually happens to be a macro) in that they take a “function” as a parameter. In the case of Object#returning
the “function” is a block. doto
is a bit more general in that it takes one or more forms, not merely functions.
But neither Object#returning
nor doto
, in general, return a function. Contrast this with const
and constantly
, both of which return functions.
Summary
Ruby on Rails’ Object#returning
, Ruby 1.9’s Object#tap
, and Clojure’s doto
are useful for clearly delineating code whose only purpose is to introduce side-effects on or about some target object. Examples include:
- tracing for debugging
- multi-step object initialization through setters
These are higher-order functions in that they each take a functional argument (or in doto
’s case, one or more). None of them, however, in general, produces a function.
The K combinator, as exemplified by Haskell’s const
and Clojure’s constantly
, is useful in cases where you need to succinctly construct a function that, irrespective of its argument(s), always returns the same value. A K combinator is a higher-order function that produces a function.
Object#returning
, Object#tap
, and doto
are not K combinators. const
and
constantly
are. These are two useful, but distinct, concepts.
Footnotes
[1] Whereas returning
yields its first parameter to the
block, tap
yields self
and so it only needs a block parameter.