Lambda composition in ruby 2.6

January 15, 2019 – David Bourguignon – 4-minute read

This article was written before Drivy was acquired by Getaround, and became Getaround EU. Some references to Drivy may therefore remain in the post

What are we talking about?

We recently updated a sizeable application to ruby 2.5, which opened up some nice features for us, such as the yield_self feature.

But I also wanted to have a quick look at 2.6 for comparison purposes, and I found a small feature that can easily be overlooked: the new proc composition operators: << and >>.

You can find the original request (from 2012!) here.

This is a way to compose a new proc by “appending” several other procs together.

Note: all the code that is present in this article can also be found in this gist

A simple example

# This lambda takes one argument and returns the same prefixed by "hello"
greet = ->(val) { "hello #{val}" }
# This lambda takes one argument and returns the upcased version
upper = ->(val) { val.upcase }

# So you can do
puts greet["world"]    # => hello world
puts upper["world"]    # => WORLD

present = greet >> upper

puts present["world"]  # => HELLO WORLD

present = greet << upper

puts present["world"]  # => hello WORLD

Lines 2 and 4 declare 2 simple lambdas, taking 1 argument each.

Lines 10 and 14 are where the magic happens.

This works like a shell-redirect operator, it takes the “output” from one lambda and sets it as input of the other.

In line 10, we take the “world” input, pass it to greet, then take the output to pass it to upper.

The equivalent would be doing: upper[greet["world"]]

Line 14 is the same in reverse order. The equivalent is greet[upper["world"]]

Let’s dive deeper

That was a simplistic example. Let’s try something more useful.

Let’s say we have a transformation-rules directory, and some pipeline definition that would define which rules we should use in a particular case.

Let’s define some pricing rules:

# List of our individual pricing rules
TAX           = ->(val) { val + val*0.05 }
FEE           = ->(val) { val + 1 }
PREMIUM       = ->(val) { val + 10 }
DISCOUNT      = ->(val) { val * 0.90 }
ROUND_TO_CENT = ->(val) { val.round(2) }
# One presenter
PRESENT       = ->(val) { val.to_f }

# Pre-define some rule sets for some pricing scenarios
REGULAR_SET    = [FEE, TAX, ROUND_TO_CENT, PRESENT]
PREMIUM_SET    = [FEE, PREMIUM, TAX, ROUND_TO_CENT, PRESENT]
DISCOUNTED_SET = [FEE, DISCOUNT, TAX, ROUND_TO_CENT, PRESENT]

Now we can define a price calculator:

def apply_rules(rules:, base_price:)
  rules.inject(:>>).call(base_price)
end

At this point we can easily calculate the pricing for a given scenario:

amount = BigDecimal(100)

puts "regular:    #{apply_rules(rules: REGULAR_SET, base_price: amount)}"    # => 106.05
puts "premium:    #{apply_rules(rules: PREMIUM_SET, base_price: amount)}"    # => 116.55
puts "discounted: #{apply_rules(rules: DISCOUNTED_SET, base_price: amount)}" # => 95.45

Again, this is quite a naive implementation.

Here we can find that the order of the rules and the type of operator we use will change, for instance in our case we want to apply taxes on the final amount (including discount, premium or other). Rounding as a last step is important too.

Is using these composition operators a good idea?

I’ll let you make your own mind up.

This is one more tool in the ruby toolbox. It’s one important step toward a more functional style, like yield_self in 2.5 (now aliased as then).

Both allow for some kind of pipeline, yield_self for transforming values, and <</>> for transforming lambdas.

Did you enjoy this post? Join Getaround's engineering team!
View openings