The letter A styled as Alchemists logo. lchemists Syndication Icon

Putin's War on Ukraine - Watch President Zelenskyy's speech and help Ukraine fight against the senseless cruelty of a dictator!

Published September 15, 2022 Updated September 15, 2022
Cover
Ruby Function Composition

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 layer function composition on top.

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 next.

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 two major behavioral differences between procs and lambdas which are: arguments and returns.

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 the what 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.

Blocks

Blocks are procs too! Example:

def build_proc(&block) = block

multiplier = build_proc { |number, by = 3| number * by }

multiplier.class  # #<Proc:0x0000000106545f40 (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, lambda, 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.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.

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 call it similarly:

Multiplier.new.call 3     # 9
Multiplier.new(5).call 3  # 15

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 }
subtracter = -> 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 of 10 on the right and then read from left to right.

  • #<< (backward composition): Start with the input of 10 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 >> subtracter >> divider).call 10  # 12
(divider << subtracter << 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 = subtracter >> 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:

  1. Success

  2. Failure

    1. Failure

      1. Success

  3. 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:

  1. Success

  2. Failure

  3. Failure

  4. 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.