The release of Ruby 2.6 in December 2018 included the following “notable new feature”:
Add function composition operators
<<
and>>
toProc
andMethod
. [Feature #6284]
In this post, I will explain “function composition” and how these new operators work, including how you can use them not just with Proc
and Method
but with any object that implements call
.
Before we get into Ruby’s implementation of function composition, let’s be clear what it actually means. Wikipedia describes function composition as follows:
In computer science, function composition is an act or mechanism to combine simple functions to build more complicated ones. Like the usual composition of functions in mathematics, the result of each function is passed as the argument of the next, and the result of the last one is the result of the whole.
Let’s think of a simple example of a function: something that takes a single number as input and returns double that number, e.g.
double(x) = x * 2
double(2) = 4
double(3) = 6
double(4) = 8
Now let’s imagine another simple function that squares a number (that is, multiplies it by itself):
square(x) = x * x
square(2) = 4
square(3) = 9
square(4) = 16
If we wanted to first double a number and then square the result, we could call each function individually with our desired input, e.g.
double(2) = 4
square(4) = 16
As our functions only calculate their result from their given input and have no side-effects (see “referential transparency” for more information on this topic), we can combine them:
square(double(2)) = 16
For convenience, we could define a new function to do this operation for us:
double-then-square(x) = square(double(x))
double-then-square(2) = 16
double-then-square(3) = 36
double-then-square(4) = 64
Tada! We have composed the two functions double
and square
into a new one, the pithily-named double-then-square
!
While composing functions yourself might seem relatively straightforward, some programming languages have a “first-class” notion of function composition which allow us to compose functions without having to define new functions ourselves. Perhaps the most concise example is Haskell’s function composition through the .
operator:
doubleThenSquare = square . double
As in our previous example, this returns a new function which will call double
and then call square
with the result, returning the final result to us.
The order of operations might be confusing here: reading left to right, we first refer to square
and then to double
but when we call our doubleThenSquare
function the order in which our component functions are called is in the opposite order. This is because the mathematical definition of function composition is as follows:
(g ∘ f)(x) = g(f(x))
This notation can be read “g
after f
” (or “g
following f
”) which might help when remembering the order of application when you call your composite function.
In a programming language where programs are largely written as a series of functions, having first-class function composition makes it easier for authors to compose behaviour from existing functions, possibly encouraging the breaking down of large functions into smaller, simpler parts.
So where does Ruby come into this?
While we’ve been discussing functions in pseudocode, we need to establish the equivalent building blocks in Ruby. Ideally we want functions that we can pass as arguments to other functions, store in variables and data structures and be able to return them from other functions: in other words, we want first-class functions.
The first obvious Ruby equivalent is Proc
, described in the Ruby documentation as follows:
A
Proc
object is an encapsulation of a block of code, which can be stored in a local variable, passed to a method or another Proc, and can be called. Proc is an essential concept in Ruby and a core of its functional programming features.
We can create a Proc
in a surprising number of ways:
# Use the Proc class constructor
double = Proc.new { |number| number * 2 }
# Use the Kernel#proc method as a shorthand
double = proc { |number| number * 2 }
# Receive a block of code as an argument (note the &)
def make_proc(&block)
block
end
double = make_proc { |number| number * 2 }
# Use Proc.new to capture a block passed to a method without an
# explicit block argument
def make_proc
Proc.new
end
double = make_proc { |number| number * 2 }
(See “Passing Blocks in Ruby Without &block
” for more information on this last constructor.)
We can use a Proc
by calling its call
method with any arguments we desire. There are also a few shorthand alternatives we can use for brevity:
double.call(2) # => 4
double.(2) # => 4
double[2] # => 4
double === 2 # => 4
Note that this last form is particularly useful when using a Proc
in the when
clause of a case
statement as case
statements evaluate their various branches by calling ===
:
divisible_by_3 = proc { |number| (number % 3).zero? }
divisible_by_5 = proc { |number| (number % 5).zero? }
case 9
when divisible_by_3 then puts "Fizz"
when divisible_by_5 then puts "Buzz"
end
# Fizz
# => nil
Proc
s also come in an extra flavour: a “lambda”. These have their own constructors:
# Use Kernel#lambda
double = lambda { |number| number * 2 }
# Use the Lambda literal syntax
double = ->(number) { number * 2 }
The key differences between “lambdas” and “procs” (that is, non-lambdas) are as follows:
return
from a lambda will only exit from the lambda whereas calling return
from a proc will exit from the surrounding method (and will throw a LocalJumpError
if there is no surrounding method)def
) but in procs, missing arguments are replaced with nil
, single array arguments are deconstructed if the proc has multiple arguments and no error is raised if too many arguments are passedWe can demonstrate these differences with the following examples:
def lambda_with_bad_arguments
lambda = ->(x, y) { "#{x} and #{y}" }
lambda.call([1, 2, 3])
end
lambda_with_bad_arguments
# ArgumentError: wrong number of arguments (given 1, expected 2)
def lambda_return
lambda = -> { return }
lambda.call
"This will be reached"
end
lambda_return
# => "This will be reached"
def proc_with_bad_arguments
proc = proc { |x, y| "#{x} and #{y}" }
proc.call([1, 2, 3])
end
proc_with_bad_arguments
# => "1 and 2"
def proc_return
proc = proc { return }
proc.call
"This will not be reached"
end
proc_return
# => nil
The other Ruby feature that we can use as a first-class function is Method
. This allows us to represent methods defined on objects (e.g. with def
) as objects themselves:
class Greeter
attr_reader :greeting
def initialize(greeting)
@greeting = greeting
end
def greet(subject)
"#{greeting}, #{subject}!"
end
end
greeter = Greeter.new("Hello")
greet = greeter.method(:greet)
# => #<Method: Greeter#greet>
These can then be called in exactly the same way as Proc
:
greet.call("world") # => "Hello, world!"
greet.("world") # => "Hello, world!"
greet["world"] # => "Hello, world!"
greet === "world" # => "Hello, world!"
Finally, some Ruby objects can transform themselves into Proc
by implementing to_proc
. Ruby will automatically call this method on any object that is being passed as a block argument to another method with &
.
[1, 2, 3].select(&:even?)
# is equivalent to
[1, 2, 3].select(&:even?.to_proc)
Ruby provides implementions of to_proc
for the following objects out of the box:
Hash
will return a Proc
that takes a key as input and returns the corresponding value from the hash or nil
if it is not foundSymbol
will return a Proc
that takes an object and will call the method with the symbol’s name on it{ name: "Alice", age: 42 }.to_proc.call(:name)
# => "Alice"
:upcase.to_proc.call("hello")
# => "HELLO"
(For more information about implementing to_proc
on objects, see “Data Structures as Functions (or, Implementing Set#to_proc
and Hash#to_proc
in Ruby)”.)
In 2012, Pablo Herrero proposed that Ruby should offer composition for Proc
with the following example:
It would be nice to be able to compose procs like functions in functional programming languages:
to_camel = :capitalize.to_proc add_header = ->val {"Title: " + val} format_as_title = add_header << to_camel << :strip
instead of:
format_as_title = lambda {|val| "Title: " + val.strip.capitalize }
Herrero provided a pure Ruby implementation which would add <<
as a method on Proc
and behave as follows:
double = ->(number) { number * 2 }
square = ->(number) { number * number }
double_then_square = square << double
double_then_square.call(2)
# => 16
That same day, a debate over the exact syntax Ruby should adopt for this feature began. Some argued for +
, others for *
(as a close approximation of Haskell's aforementioned syntax) and some for |
. In October that year, Matz weighed in:
Positive about adding function composition. But we need method name consensus before adding it? Is
#*
OK for everyone?
Unfortunately, consensus was not forthcoming (with <-
being added to the debate) over the next two years.
In 2015, after spending some time doing functional programming in Clojure and finding the dormant proposal, I contributed a series of patches to Ruby to implement composition on Proc
and Method
. Keen to progress the debate, I picked *
as the operator:
double_then_square = square * double
double_then_square.call(2)
# => 16
Herrero responded by proposing syntax inspired by F#’s function composition:
It would be nice to be able to compose functions in both ways, like in F#, you can do g << f or g >> f, sadly this was rejected before.
These two operators would allow for both “backward” composition with <<
and “forward” composition with >>
.
“Backward” composition maps to the mathematical operator ∘
we discussed earlier so g << f
is the same as g ∘ f
meaning that calling the resulting composite function with an input x
will call g(f(x))
double_then_square = square << double
double_then_square.call(2)
# => 16
“Forward” composition is the opposite of the above so g >> f
is the same as f ∘ g
meaning that calling the resulting composite function with an input x
will call f(g(x))
double_then_square = double >> square
double_then_square.call(2)
# => 16
It might be helpful to think of the direction of << and >> as indicating how results flow from f
to g
and vice versa, e.g. the result of f
is passed to g
in g << f
whereas the result of g
is passed to f
in g >> f
.
Another three years passed and the feature was finally merged into Ruby 2.6 by Nobuyoshi Nakada who took my original patches and changed the operator from *
to <<
and >>
.
So now we know what function composition is and how it got into Ruby 2.6, how does it work?
There are two new operators:
<<
: called “backward composition” in F# and “compose to left” in the Ruby source code>>
: called “forward composition” in F# and “compose to right” in the Ruby source codeHere are some examples of usage with our existing double
and square
:
double_then_square = square << double
double_then_square.call(2) # => 16
# or
double_then_square = double >> square
double_then_square.call(2) # => 16
These operators are implemented on both Proc
(regardless whether they are lambdas or procs) and on Method
.
Internally, the way composition works regardless whether you’re using <<
or >>
is to create a new Proc
(preserving whether the receiver is a lambda or not) that composes our functions for us. The entire feature is roughly equivalent to the following Ruby code:
class Proc
def <<(g)
if lambda?
lambda { |*args, &blk| call(g.call(*args, &blk)) }
else
proc { |*args, &blk| call(g.call(*args, &blk)) }
end
end
def >>(g)
if lambda?
lambda { |*args, &blk| g.call(call(*args, &blk)) }
else
proc { |*args, &blk| g.call(call(*args, &blk)) }
end
end
end
class Method
def <<(g)
to_proc << g
end
def >>(g)
to_proc >> g
end
end
This means we can compose Proc
and Method
objects in various configurations:
(double >> square).call(2)
# => 16
(double >> square >> Kernel.method(:puts)).call(2)
# 16
# => nil
(Kernel.method(:rand) >> square >> Kernel.method(:puts)).call
# 0.010775469851890788
# => nil
It also means that if you start with a lambda, composing to the left and the right will always produce a new lambda:
square.lambda?
# => true
not_a_lambda = proc { nil }
not_a_lambda.lambda?
# => false
(square >> not_a_lambda).lambda?
# => true
(square << not_a_lambda).lambda?
# => true
Similarly, starting with a non-lambda Proc
or a Method
will never give you a lambda back regardless of the right-hand operand:
(not_a_lambda >> square).lambda?
# => false
(not_a_lambda << square).lambda?
# => false
We can also take advantage of calling to_proc
on Ruby objects and compose these with regular Proc
objects:
(
{ name: "Alice", age: 42 }.to_proc >>
:upcase.to_proc
).call(:name)
# => "ALICE"
The other key thing to note when composing is that the argument can be a Proc
, a Method
or anything that responds to call
. This allows us to compose our own objects with Proc
:
class Greeter
attr_reader :greeting
def initialize(greeting)
@greeting = greeting
end
def call(subject)
"#{greeting}, #{subject}!"
end
end
(
:upcase.to_proc >>
Greeter.new("Hello") >>
Kernel.method(:puts)
).call("world")
# Hello, WORLD
# => nil
Note this only works if your receiver is a Proc
or a Method
though, so you couldn’t put Greeter
first in the chain above (as it does not implement >>
).
If you want to read more about example use cases for function composition in Ruby, please see the following blog posts:
It’s also interesting to look at the use of function composition in other languages:
Paul Mucur is a software development consultant at Ghost Cassette, open source maintainer and Ruby and Ruby on Rails contributor.