
The ability to compose functions has been available since Ruby 2.6.0 where the #>> and #<< methods were added to the Proc
and Method
classes so you can compose your functions as series of logical steps. The computer science definition — as pulled from from Wikipedia — is:
[F]unction 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.
The rest of this article will dive into what function composition looks like in Ruby and how you can leverage it in your own code.
Functions
The foundation to unlocking functional programming within Ruby is through the use of procs, lambdas, blocks, methods, and objects which respond to #call
. The following will walk you through what functional programming in Ruby looks like so we can eventually compose our functions together for maximum effect.
Procs
Procs can be defined by using either Proc.new
or Kernel#proc
:
multiplier = Proc.new { |number, by = 3| number * by }
multiplier = proc { |number, by = 3| number * by }
Both of the above are identical and can be verified by inspecting the function:
multiplier.class # Proc
multiplier.inspect # #<Proc:0x00000001059b5248 (irb):22>
💡 Take note of the class and instance, shown above, because there is a subtle distinction which I’ll point out when talking more about lambdas soon.
Once a proc is defined, you use it by sending the #call
message:
multiplier.call 3 # 9
multiplier.call 3, 10 # 30
Use of #call
is the primary Object API and foundation for functional programming in Ruby. By the way, shorthand syntax for calling procs — and lambdas as well — is:
multiplier.(3) # 9
multiplier[3] # 9
multiplier === 3 # 9
Despite different ways to call a proc, the #call
method is the preferred form since it’s the most intuitive to read. Lastly, in regards to case equality, it is rare in use but valuable when using case
statements since branches are evaluated using the triple equals (i.e. #===
) method. Example:
def check text
downcase = proc { |text| text == text.downcase }
upcase = proc { |text| text == text.upcase }
case text
when downcase then "Text is downcased."
when upcase then "Text is upcased."
else "Text is unknown."
end
end
check "test" # Text is downcased.
check "TEST" # Text is upcased.
check "Test" # Text is unknown.
Now that we are on the same page with procs, we can nicely segue into lambdas.
Lambdas
Lambdas are procs but with a few modifications. We’ll get to the differences in a moment but here’s how you define them using either Kernel#lambda
or literal (i.e. stabby) syntax:
multiplier = lambda { |number, by = 3| number * by }
multiplier = -> number, by = 3 { number * by }
Despite the difference in syntax between a proc and a lambda, we can verify the above is still a Proc
by inspecting the function’s class:
multiplier.class # Proc
multiplier.inspect # "#<Proc:0x0000000104ba68a0 (irb):19 (lambda)>"
The subtle distinction — as hinted at earlier — is seeing (lambda)
show up when inspecting the instance. Otherwise, everything else about procs applies to lambdas except for a few differences which will be discussed next.
Procs versus Lambdas
There are a few behavioral differences between procs and lambdas that are worth being aware of and are explained in detail below.
Arguments
When it comes to arguments, the easiest way to think about them is procs are relaxed while lambdas are strict. Example:
proc_echo = proc { |number| number }
lambda_echo = -> number { number }
proc_echo.call # nil
lambda_echo.call # ArgumentError: wrong number of arguments
With the proc example, echoing nil
(no argument) is…well…nil
because a proc doesn’t check if you supplied the necessary arguments or not. On the flip side, lambdas care about arguments which is why you get an ArgumentError
when they are missing.
Returns
How you return from a proc or lambda differs depending if the function is used inside or outside a method. Scope matters. Here’s what the behavior looks like when called inside a method:
def proc_return
function = proc { return }
function.call
"Made it!"
end
def lambda_return
function = -> { return }
function.call
"Made it!"
end
proc_return # nil
lambda_return # Made it!
When a proc or lambda is called outside a method, the behavior is more abrupt:
function = proc { return }
function.call
# LocalJumpError: unexpected return
The LocalJumpError
exception occurs because there is no surrounding method but lambdas don’t have this issue:
function = -> { return }
function.call
# nil
Instead of an exception you get nil
since there was nothing returned.
Inquiry
In situations where inspection isn’t enough, you can ask if a proc is a lambda:
a_proc = proc { "I'm a proc" }
a_lambda = -> { "I'm a lambda" }
a_proc.lambda? # false
a_lambda.lambda? # true
Blocks
Blocks are procs too! Example:
def build_proc(&block) = block
multiplier = build_proc { |number, by = 3| number * by }
multiplier.class # Proc
multiplier.inspect # "#<Proc:0x0000000103a763a0 (irb):2>"
While the above is of little use in production code — due to the unnecessary method wrap — you can see that blocks are ultimately procs too.
Methods
In addition to procs, lambdas, and blocks, methods are powerful first-class citizens as well. Example:
module Calculate
def self.multiply(number, by = 3) = number * by
end
multiplier = Calculate.method :multiply
multiplier.class # Method
multiplier.inspect # "#<Method: Calculate#multiply(number) /snippet:22>"
multiplier.call 3 # 9
Methods can also be converted into procs too:
multiplier = Calculate.method(:multiply).to_proc
multiplier.class # Proc
multiplier.inspect # #<Proc:0x0000000104906f78 (lambda)>
A few other classes like Symbol
(which is how the classic symbol-to-proc syntax works), Hash
, and even my own Versionaire gem do this as well. To illustrate further, this means you can do this:
[1, 2, 3].map(&multiplier) # [3, 6, 9]
The above is is shorthand for:
[1, 2, 3].map { |number| multiplier.call number }
None of this is unique to methods since Procs and Lambdas work this way as well. What is unique to methods — and handy to have in your debugging toolkit — is the ability to find the source of a method:
multiplier.source_location # ["(irb)", 5]
String.instance_method(:join).source_location # nil
The first line shows the source location of my Ruby code running in IRB but the source location for String answers nil
because the implementation is written in C code. This caveat exists for core primitives like strings, arrays, hashes, etc. which are implemented in C but won’t be a problem for application code written in Ruby.
Classes
By this point, you should be seeing a strong, reinforcing, theme here where use of #call
is the glue that ties functional programming together in Ruby. The key to implementing a functional class is that must adhere to the Command Pattern. Using this knowledge, here’s what a multiplier class would look like based on previous examples:
class Multiplier
def initialize by = 3
@by = by
end
def call(number) = number * by
private
attr_reader :by
end
As with procs, lambdas, and methods, we can inspect and call it similarly:
multiplier = Multiplier.new
multiplier.class # Multiplier
multiplier.inspect # "#<Multiplier:0x0000000103761908 @by=3>"
multiplier.call 3 # 9
Multiplier.new(10).call 3 # 30
Now that we have the basics of how functional programming works in Ruby, we can move on to discussing function composition!
Composition
To set the stage in understanding function composition, we’ll compose a series of functions using everything discussed thus far. Here’s the code:
module Composable
def >>(other) = method(:call) >> other
def <<(other) = method(:call) << other
def call = fail NotImplementedError, "`#{self.class.name}##{__method__}` must be implemented."
end
class Divider
include Composable
def initialize by = 3
@by = by
end
def call(number) = number / by
private
attr_reader :by
end
module Calculate
def self.multiply(number, by = 3) = number * by
end
adder = proc { |number, by = 3| number + by }
subtractor = -> number, by = 3 { number - by }
divider = Divider.new
multiplier = Calculate.method(:multiply)
As you can see, we have a proc, lambda, class, and method. All of which are functions we can compose together. What’s new is the Composable
module which is extracted from the Transactable gem (more on this gem in a moment). The Composable
module allows the Divider
class to be composed in any order (more on this in a moment too).
We can compose the above functions multiple ways but we’ll start with a couple:
(adder >> multiplier).call 10 # 39
(multiplier << adder).call 10 # 39
Notice we get the same result in both cases, only the order of operations changes based on whether you use the #>>
or the #<<
method. Also, the equation is the same for both too:
(10 + 3) * 3 = 39
The way to read each example can be explained as:
-
#>>
(forward composition): Start with the input of10
on the right and then read from left to right. -
#<<
(backward composition): Start with the input of10
on the right and then continue to read from right to left.
Both ways of reading the code is slightly awkward and you could make a case for either approach being better or worse in terms of readability. Personally, I like #>>
because reading left to right is more natural despite the awkwardness of 10
(input) being on the right side to start with. Whatever your style, please be consistent because reading code that uses both directions is taxing.
Returning back to the code, here’s how you could use all functions at once:
# Equation: (((10 + 3) * 3) - 3) / 3 = 12
(adder >> multiplier >> subtractor >> divider).call 10 # 12
(divider << subtractor << multiplier << adder).call 10 # 12
Using only forward composition, you could refactor the above into smaller functions:
add_and_multiply = adder >> multiplier
subtract_and_divide = subtractor >> divider
add_multiply_subtract_and_divide = add_and_multiply >> subtract_and_divide
add_multiply_subtract_and_divide.call 10 # 12
Granted, the above is contrived but you can, at least, see the potential of building multiple levels of functions all composed together to yield a final result.
A word of caution — because I don’t want to give the wrong impression — the order of how your functions are composed matters. For example, toggling between forward and backward composition without also changing the order of operations will yield different results:
(adder >> multiplier).call 10 # 39
(adder << multiplier).call 10 # 33
Additionally, any error in the series will halt further operations. Example:
(adder >> Divider.new(0) >> multiplier).call 10 # ZeroDivisionError: divided by 0
In this situation, I’ve initialized a new Divider
instance which always divides by zero to simulate a situation in which we get an exception which may or may not be expected. This is one downside of function composition because, if any function within the series of functions fails, the whole composition fails much like how a faulty light in a series of lights causes all lights to go out during the holidays. We can do better, though, which leads us to the Transactable gem I’ve been hinting about.
Transactable
The Transactable gem builds upon functional composition by enhancing it through the use of the Railway Pattern. In short, this means you can think of each function being composed of a series of steps which are part of a pipe. Even better, any one one of the steps is either a Success
or a Failure
but not an exception. This means you have the opportunity to turn a failure into a success in subsequent steps which gives you an architecture with failsafes built in. Visually, you can view the Railway Pattern in terms of an outline:
-
Success
-
Failure
-
Failure
-
Success
-
-
-
Success
The above starts out successful, fails, recovers, and ultimately ends up successful. Failsafes might not always be possible so you could end up with a situation in which things start out successful but ultimately fail. Example:
-
Success
-
Failure
-
Failure
-
Failure
The purpose of these examples is that — for each step — you’ll have either a Success
or Failure
which you can act upon accordingly. The steps don’t immediately break if there is one bad apple so you have the ability to recover if desired. To expand upon this further, here’s a script that you can run locally to see Transactable in action:
#! /usr/bin/env ruby
# frozen_string_literal: true
# Save as `snippet`, then `chmod 755 snippet`, and run as `./snippet`.
require "bundler/inline"
gemfile true do
source "https://rubygems.org"
gem "amazing_print"
gem "debug"
gem "http"
gem "dry-monads"
gem "transactable"
end
include Dry::Monads[:result]
class Pinger
include Transactable
def initialize client: HTTP
@client = client
end
def call url
pipe url,
tee(Kernel, :puts, "Checking: #{url}..."),
check(/\Ahttps/, :match?),
method(:get),
as(:status),
method(:report)
end
private
attr_reader :client
def get result
result.fmap { |url| client.timeout(1).get url }
rescue HTTP::TimeoutError => error
Failure error.message
end
def report(result) = result.fmap { |status| status == 200 ? "Site is up!" : status }
end
url = "https://xkcd.com"
case Pinger.new.call(url)
in Success(message) then puts "Success: #{message}"
in Failure(error) then puts "Site is down or invalid. Reason: #{error}"
end
If you skip past the Bundler Inline setup, you’ll notice there is a Pinger
class — which adheres to the Command Pattern — where the #call
method pipes to a series of steps. We can iterate through these steps via code comments:
pipe url, # The URL (raw input).
tee(Kernel, :puts, "Checking: #{url}..."), # Print info to the console.
check(/\Ahttps/, :match?), # Check if the URL is secure.
method(:get), # Make the HTTP GET request.
as(:status), # Ask the HTTP response for status.
method(:report) # Report on the HTTP status.
If you modify the script’s url
each time you run the script, you’ll different behavior. Example:
# https://xkcd.com Checking: https://xkcd.com... Success: Site is up! # http://xkcd.com Checking: http://xkcd.com... Site is down or invalid. Reason: http://xkcd.com # https://www.unknown.com Checking: https://www.unknown.com... Site is down or invalid. Reason: Timed out after using the allocated 1 seconds
Based on the above output, you can see success and failure. Recovery is possible too but I’ll leave that up to you to experiment with further (hint: use the #orr
step). Be sure to check out the Transactable documentation for additional details.
Caveat
Earlier, I mentioned that in order to use classes in function composition, you only needed to implement the #call
method. For the most part, this is a true statement. However, there is one subtle caveat to this approach which is you can’t use an instance which only responds to #call
in the first position of your composition. To explain, let’s return to the code again:
# Without the `Composable` module included in the `Divider` class.
(divider >> adder).call 12 # NoMethodError: undefined method `>>'
# With the `Composable` module included in the `Divider` class.
(divider >> adder).call 12 # 7
The reason the first example fails is because, by default, a class instance doesn’t implement the forward or backward composition methods. Due to this situation, the Transactable gem solves this issue by providing the Composable
module so a functional instance can be composed in any order without error.
Conclusion
I hope you’ve enjoyed this deep dive into functional programming in Ruby, learning how to compose your functions into more sophisticated functionality, and seeing how you can take this further via the Transactable gem.
I love that Ruby allows us to marry Objected Oriented Programming with Functional Programming principals with minimal effort. By leveraging these patterns, you benefit from a more elegant design and powerful architecture.