Source: unsplash.com

Function Composition in Ruby

Tech - RubyCademy
RubyCademy

--

In this article, we’re going to explore the following topics:

  • how to use Function Composition in Ruby
  • a human-friendly syntax
  • how it works with lambdas
  • Function Composition and Duck Typing
  • Function Composition behind the scene

Function Composition

In CompSci, the notion of Function Composition refers to the act of combining multiple functions to build more complicated ones. In Ruby, this notion is represented by the Proc#<< and Proc#>> methods

increment = proc {|x| x + 1}
double = proc {|x| x * 2}
(double << increment).call(5) # => 12

Let’s detail step-by-step what occurred in the previous example

increment = proc {|x| x + 1}
double = proc {|x| x * 2}
x = 5
x = increment.call(x) # => 5 + 1 = 6
double.call(x) # => 6 * 2 = 12

As we use the left-shift operator, the right-hand proc (increment) is executed first and the result of this proc call is passed as an argument of the left-hand proc call (double).

The mechanism is the same for the right-shift operator except that the left-hand proc is executed first.

A human-friendly syntax

The syntax provided by Ruby is human-friendly and it gives you the opportunity to ensure the same result in many different ways.

This approach gives you the opportunity to express the solution directly from your thoughts.

Indeed, while in other languages you must comply with unique syntax and play with the order of execution to ensure the right result for your composition, in Ruby you don’t have to deal with this inconvenience.

Also, the syntax is way more readable with Ruby.

Let’s compare how to use Function Composition in Python and Ruby.

In Python

import functoolsdef double(x):
return x * 2
def inc(x):
return x + 1
def dec(x):
return x - 1
def compose(*functions):
return functools.reduce(lambda f, g: lambda x: f(g(x)), functions, lambda x: x)
func_comp = compose(dec, double, inc)
func_comp(10) # => 21

In Ruby

double = proc { |x| x * 2 }
inc = proc { |x| x + 1 }
dec = proc { |x| x - 1 }
(dec << double << inc).call(10) # => 21

You must agree that the syntax is way more natural and readable in Ruby. Also, in Python, you must play with the order of the methods passed as arguments of the compose method call to ensure the expected result.

In Ruby, you can change the order of execution while ensuring the same result without risking cluttering your source code

(double << inc >> dec).call(10) # => 21
(dec << double << inc).call(10) # => 21
(inc >> double >> dec).call(10) # => 21

Function Composition with lambdas

Firstly, feel free to have a look to the Proc vs Lambda in Ruby article if you’re not familiar with the differences between these two idioms.

As lambdas and procs are both instances of the Proc class, Function Composition can be used with both of these tools

double = proc   { |x| x * 2 }
inc = lambda { |x| x + 1 }
dec = proc { |x| x - 1 }
(dec << double << inc).call(10) # => 21

As lambdas are strict on argument numbers you must be careful to avoid side effects

double = proc   { |x| x * 2}
add = lambda { |x, y| x + y }
(double << add).call(1)

produces

ArgumentError (wrong number of arguments (given 1, expected 2))

So, to fix this, we can pass a second argument to the call method

double = proc   { |x| x * 2}
add = lambda { |x, y| x + y }
(double << add).call(1, 2) # => 6

Indeed, here the result of the add lambda is passed as a single argument of the double proc.

So (1 + 2) * 2 = 6.

Function Composition and Duck Typing

So what if I tell you that Function Composition in Ruby doesn’t work exclusively with procs and lambdas?

class Triple
def call(x)
x * 3
end
end
increment = ->(x) { x + 1 }
triple = Triple.new
(increment << triple).call(1) # => 4

Here triple is an instance of Triple that responds to the call method. Indeed, Proc#<< and Proc#>> methods expect that the object passed as an argument responds to the call method. The only constraint is that the first function from the left must be an instance of Proc or one of its specializations.

This implementation follows the rules of the Duck Typing design principle:

When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.

So we can assume that

If an object acts like a proc (by responding to the call method), just go ahead and treat it as a proc.

Feel free to have a look to the Why the Ruby community encourages Duck Typing article if you’re not familiar with this design principle.

Function Composition behind the scene

The Proc#>> and Proc#<< methods return a new instance of Proc that will execute the first proc and pass the return value of this proc call as an argument of the second proc to execute. This mechanism is triggered when you invoke call on this freshly created proc

a = proc { 42 }
b = proc { |x| x * 2 }
composed_proc = (a >> b) # => #<Proc:0x00007f95820bb908>
composed_proc.call # => 84

Here we can see that a >> b returns a new proc and when we invoke composed_proc.call then b.call(a.call) is executed.

Now let’s see what happens when we use composition with lambdas and procs

a = lambda { |x| x * 2 }
b = proc { 42 }
composed_proc = (a << b) # => #<Proc:0x00007f99fd054a80(lambda)>
composed_proc.call # => 84

Here we can see that composed_proc is a lambda. Indeed, the type of the left-hand proc defines the type of composed_proc. So, as a is a lambda then composed_proc is also a lambda. Now, let’s invert a and b in our composition

a = lambda { |x| x * 2 }
b = proc { 42 }
composed_proc = (b << a) # => #<Proc:0x00007f99fd054a80>
composed_proc.call

produces

ArgumentError (wrong number of arguments (given 0, expected 1))

Here, b is a proc. So composed_proc is also a proc. The ArgumentError is raised because composed_proc.call invoke a.call first and as a is a lambda it strictly expects an argument.

It’s also the left-hand proc that defines the type of the composed proc when using the Proc#>> method. Indeed, as the left-hand proc is the receiver of the Proc#>> and Proc#<< message, it’ll automatically define the type of the returned proc according to the type of self — the instance that receives this method call.

You can assume this behavior until Ruby 2.7.1. The next version of Ruby seems to slightly modify this behavior by using the type of the right-hand proc for the Proc#<< method.

Ruby Mastery

We’re currently finalizing our first online course: Ruby Mastery.

Join the list for an exclusive release alert! 🔔

🔗 Ruby Mastery by RubyCademy

Also, you can follow us on x.com as we’re very active on this platform. Indeed, we post elaborate code examples every day.

💚

--

--