Ruby's new function composition syntax
Ruby's new function composition syntax

Function composition >> Ruby

Last week Proc#<< and Proc#>> got merged into Ruby 2.6. This opens the door for function composition. Here’s my opinion as to why this is a great leap forward for Ruby and what needs to improve.

Composition vs. inheritance

Ruby is an object-oriented language (among others), meaning it has the concept of an object and the class of an objects — which has the concept of inheritance.

With inheritance we are able to create objects with specialized behavior by inheriting the behavior of a more generalized object (the parent) and specializing it. Let’s build a client for an API that consumes both JSON and XML encoded data. It could look like this.

# Wrapper around a HTTP library
class ApiClient; ... end

# Knows how to decode JSON responses from the API
class JSONApiClient < ApiClient; ... end

# Knows how to decode XML responses from the API
class XMLApiClient < ApiClient; ... end

# Exposes an API's endpoints as methods on an object
class MyAppApiClient
  attr_reader :json_client
  attr_reader :xml_client

  def initialize(api_token)
    @json_client = JSONApiClient.new(api_token)
    @xml_client = XMLApiClient.new(api_token)
  end

  def current_account
    json_client.get('/api/current_account')
  end

  def balance
    xml_client.get('/api/current_account/balance')
  end
end

This certainly would get the job done. But this code has two issues that we need to address.

First, both JSONApiClient and XMLApiClient are tightly coupled to their parent and provide little functionality besides the functionality they inherited — meaning that any change to the parent class would probably break the classes inheriting from it.

Second, JSONApiClient and XMLApiClient are too specialized. Inheritance is all about specialization, but having too specialized classes can cause headaches — they are hard to extend (when requirements change) and introduce unnecessary complexities (for a human, it’s hard to remember many different kinds of objects and what they do).

We can address both issues by passing the desired parser as an argument to ApiClient. This is in-line with the open-close principle, as now we can create an ApiClient that can parse any kind of data without needing to subclass it.

class MyAppApiClient
  attr_reader :json_client
  attr_reader :xml_client

  def initialize(api_token)
    @json_client = ApiClient.new(API_KEY, JSON.method(:parse).to_proc)
    @xml_client = ApiClient.new(API_KEY, XML.method(:parse).to_proc)
  end

  def current_account
    json_client.get('/api/current_account')
  end

  def balance
    xml_client.get('/api/current_account/balance')
  end
end

We took an existing class/service/object/function and combined it with another to get more specialized behavior. This is the basic idea behind function composition.

While the above example is actually an example of dependency injection, it is a great outline for the things that are possible with function composition in Ruby. Also, note that the above approach is limited to the configurable dependencies we accept through the initializer.

We use composition every day without even noticing it. Enumerable#map uses composition to enable us to transform arrays.

array = [1,2,3,4,5]

array.map # => #<Enumerator: ...>

array.map { |i| i * 2 } # => [2, 4, 6, 8, 10]

The map method is of limited usefulness on it’s own. But when we combine it with a block (another function) it can transform whole datasets.

Proc#>> and Proc#<<

Proc#>> and Proc#<< were introduced in Ruby 2.6. They provide a substantial quality-of-life improvement when it comes to composing functions.

Proc#>> is similar to Elixir’s pipeline, but instead of returning a result it returns a proc — a callable object.

f = -> x { x + 2 }
g = -> x { x * 2 }

# h is the composition of f and g
h = f >> g
# h is the same as -> x { (x + 2) * 2 }

[1, 2, 3].map(&h) # => [6, 8, 10]
# This is exactly the same as
[1, 2, 3].map(&f).map(&g) # => [6, 8, 10]

In mathematical terms, f(x) >> g(x) is the same as g(f(x)).

Let’s go back to our ApiClient example:

fetch_transactions =
  ApiClient.new(api_token).method(:get)
  >> JSON.method(:parse)
  >> (-> response { response['data']['transactions'] })

fetch_transaction.call('/api/current_user/transactions')

Notice that we didn’t pass anything to ApiClient, we didn’t need to do dependency injection. This solves the configuration problem we had before. And we are able to create highly specialized functions on-the-fly. The above example creates a function that returns all transaction from an API endpoint.

Note, if you want Elixir style pipelines that return a result check out yield_self or the new alias for it then.

On the other hand, Proc#<< is more in-line with Haskell style composition:

f = -> x { x + 2 }
g = -> x { x * 2 }

# h is the composition of g and f
h = f << g
# h is the same as -> x { (x * 2) + 2 }

[1, 2, 3].map(&h) # => [4, 6, 8]
# This is exactly the same as
[1, 2, 3].map(&g).map(&f) # => [4, 6, 8]

Or, in mathematical terms, f(x) << g(x) is the same as f(g(x)).

Both << and >> are basically the same, which one to use is only subject to your preference.

Conclusion

Function composition is very useful in languages that don’t have the concept of classes and inheritance as it enables you to “inherit” the behavior of a function and extend/specialize its behavior. Like in the following example.

require 'net/http'
require 'uri'
require 'json'

fetch =
  self.method(:URI) \
  >> Net::HTTP.method(:get) \
  >> JSON.method(:parse) \
  >> -> response { response.dig('bpi', 'EUR', 'rate') || '0' } \
  >> -> value { value.gsub(',', '') } \
  >> self.method(:Float)

fetch.call('https://api.coindesk.com/v1/bpi/currentprice.json') # => 3530.6782

Since we do have inheritance in Ruby this kind of composition is less useful. Yet it gives us the ability to create utility functions on-the-fly thus it encouraging us to create methods/classes that can be extended through the open-close principle / dependency injection, and composition-over-inheritance. In my opinion, this is a big step forward for the language.

As its implemented now, composition sticks out like a sore thumb. The ecosystem needs to grow to accommodate for this feature. Constantly calling #method on a class/module is confusing and distracting — a short hand operator for this purpose would be welcome (I would propose &[Json].parse). The map, reduce and other enumerator methods are hard to compose since they are implemented on an instance of an object — an Enum module exposing them would be nice.

If somebody shares these pain points, and agrees with me. I’ve created a draft implementation of those features.


Analytics & backlinks Analytics & backlinks

Other popular articles
Title image for "Do you really need WebSockets?"

Do you really need WebSockets?

Over the years I've had this conversation a couple of times. This post will explain why we use WebSockets, how they can be used, what alternatives exist and when to use them. Why WebSockets? Every time I worked on a project where we had to implement any kind of a "real-time" component, usuall...

Continue reading ...

Title image for "The Judo way"

The Judo way

As a kid I practiced Judo which is a martial art as well as a philosophy. And my Judo teacher gave us a philosophical story which I came to understand only recently and which helped me achieve more than I though was possible. So I wanted to share it. The story was about a small tree in a stron...

Continue reading ...