A Guide to Function Composition in Ruby
source link: https://www.tuicool.com/articles/hit/rQjqqqn
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
by Paul Mucur on Wednesday, 8th May 2019
The release of Ruby 2.6 in December 2018 included the following “notable new feature”:
Add function composition operators <<
and >>
to Proc
and Method
. [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
.
- What is function composition?
- The Ruby feature proposal
- Function composition in Ruby
What is function composition?
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
:
<i># Use the Proc class constructor</i> double = Proc.new { |number| number * 2 } <i># Use the Kernel#proc method as a shorthand</i> double = <b>proc</b> { |number| number * 2 } <i># Receive a block of code as an argument (note the &)</i> <b>def</b> make_proc(&block) block <b>end</b> double = make_proc { |number| number * 2 } <i># Use Proc.new to capture a block passed to a method without an # explicit block argument</i> <b>def</b> make_proc Proc.new <b>end</b> 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) <i># => 4</i> double.(2) <i># => 4</i> double[2] <i># => 4</i> double === 2 <i># => 4</i>
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? } <b>case</b> 9 <b>when</b> divisible_by_3 <b>then</b> puts "Fizz" <b>when</b> divisible_by_5 <b>then</b> puts "Buzz" <b>end</b> <i># Fizz # => nil</i>
Proc
s also come in an extra flavour: a “lambda”. These have their own constructors:
<i># Use Kernel#lambda</i> double = lambda { |number| number * 2 } <i># Use the Lambda literal syntax</i> double = ->(number) { number * 2 }
The key differences between “lambdas” and “procs” (that is, non-lambdas) are as follows:
-
Calling
return
from a lambda will only exit from the lambda whereas callingreturn
from a proc will exit from the surrounding method (and will throw aLocalJumpError
if there is no surrounding method) -
In lambdas, the number of arguments must match the definition exactly (as in a method definition with
def
) but in procs, missing arguments are replaced withnil
, single array arguments are deconstructed if the proc has multiple arguments and no error is raised if too many arguments are passed
We can demonstrate these differences with the following examples:
<b>def</b> lambda_with_bad_arguments lambda = ->(x, y) { "#{x} and #{y}" } lambda.call([1, 2, 3]) <b>end</b> lambda_with_bad_arguments <i># ArgumentError: wrong number of arguments (given 1, expected 2)</i> <b>def</b> lambda_return lambda = -> { <b>return</b> } lambda.call "This will be reached" <b>end</b> lambda_return <i># => "This will be reached"</i> <b>def</b> proc_with_bad_arguments proc = proc { |x, y| "#{x} and #{y}" } proc.call([1, 2, 3]) <b>end</b> proc_with_bad_arguments <i># => "1 and 2"</i> <b>def</b> proc_return proc = <b>proc</b> { <b>return</b> } proc.call "This will not be reached" <b>end</b> proc_return <i># => nil</i>
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:
<b>class</b> Greeter <b>attr_reader</b> :greeting <b>def</b> initialize(greeting) @greeting = greeting <b>end</b> <b>def</b> greet(subject) "#{greeting}, #{subject}!" <b>end</b> <b>end</b> greeter = Greeter.new("Hello") greet = greeter.method(<b>:greet</b>) <i># => #<Method: Greeter#greet></i>
These can then be called in exactly the same way as Proc
:
greet.call("world") <i># => "Hello, world!"</i> greet.("world") <i># => "Hello, world!"</i> greet["world"] <i># => "Hello, world!"</i> greet === "world" <i># => "Hello, world!"</i>
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(&<b>:even?</b>) <i># is equivalent to</i> [1, 2, 3].select(&<b>:even?</b>.to_proc)
Ruby provides implementions of to_proc
for the following objects out of the box:
-
Hash
will return aProc
that takes a key as input and returns the corresponding value from the hash ornil
if it is not found -
Symbol
will return aProc
that takes an object and will call the method with the symbol’s name on it
{ <b>name:</b> "Alice", <b>age:</b> 42 }.to_proc.call(<b>:name</b>) <i># => "Alice"</i> <b>:upcase</b>.to_proc.call("hello") <i># => "HELLO"</i>
(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)
”.)
The Ruby feature proposal
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 = <b>:capitalize</b>.to_proc add_header = <b>-></b>val {"Title: " + val} format_as_title = add_header << to_camel << <b>:strip</b>
instead of:
format_as_title = <b>lambda</b> {|val| "Title: " + val.strip.capitalize }
Herrero provided a pure Ruby implementation which would add <<
as a method on Proc
and behave as follows:
double = <b>-></b>(number) { number * 2 } square = <b>-></b>(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 ∘
weso 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) <i># => 16</i>
“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) <i># => 16</i>
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 >>
.
Function composition in Ruby
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 code
Here are some examples of usage with our existing double
and square
:
double_then_square = square << double double_then_square.call(2) <i># => 16</i> <i># or</i> double_then_square = double >> square double_then_square.call(2) <i># => 16</i>
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:
<b>class</b> Proc <b>def</b> <<(g) <b>if</b> lambda? <b>lambda</b> { |*args, &blk| call(g.call(*args, &blk)) } <b>else</b> <b>proc</b> { |*args, &blk| call(g.call(*args, &blk)) } <b>end</b> <b>end</b> <b>def</b> >>(g) <b>if</b> lambda? <b>lambda</b> { |*args, &blk| g.call(call(*args, &blk)) } <b>else</b> <b>proc</b> { |*args, &blk| g.call(call(*args, &blk)) } <b>end</b> <b>end</b> <b>end</b> <b>class</b> Method <b>def</b> <<(g) to_proc << g <b>end</b> <b>def</b> >>(g) to_proc >> g <b>end</b> <b>end</b>
This means we can compose Proc
and Method
objects in various configurations:
(double >> square).call(2) <i># => 16</i> (double >> square >> Kernel.method(<b>:puts</b>)).call(2) <i># 16 # => nil</i> (Kernel.method(<b>:rand</b>) >> square >> Kernel.method(<b>:puts</b>)).call <i># 0.010775469851890788 # => nil</i>
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? <i># => true</i> not_a_lambda = proc { nil } not_a_lambda.lambda? <i># => false</i> (square >> not_a_lambda).lambda? <i># => true</i> (square << not_a_lambda).lambda? <i># => true</i>
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? <i># => false</i> (not_a_lambda << square).lambda? <i># => false</i>
We can also take advantage of calling to_proc
on Ruby objects and compose these with regular Proc
objects:
( { <b>name:</b> "Alice", <b>age:</b> 42 }.to_proc >> <b>:upcase</b>.to_proc ).call(<b>:name</b>) <i># => "ALICE"</i>
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
:
<b>class</b> Greeter <b>attr_reader</b> :greeting <b>def</b> initialize(greeting) @greeting = greeting <b>end</b> <b>def</b> call(subject) "#{greeting}, #{subject}!" <b>end</b> <b>end</b> ( <b>:upcase</b>.to_proc >> Greeter.new("Hello") >> Kernel.method(<b>:puts</b>) ).call("world") <i># Hello, WORLD # => nil</i>
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:
- “ Function composition >> Ruby ” by Stanko Krtalić Rusendić
- “ Lambda composition in ruby 2.6 ” by David Bourguignon
- “ Function Composition in Ruby ” by Tom Wey
It’s also interesting to look at the use of function composition in other languages:
- “ Composing functions ” in Clojure by Kumar Iyer
- “ Function Composition ” in F# by Chris Smith
Paul Mucur is a software development consultant at Ghost Cassette, open source maintainer and Ruby and Ruby on Rails contributor.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK