In Progress
Unit 1, Lesson 21
In Progress

Function Composition Operators

From the very beginning, Ruby has always supported a multi-paradigm approach to programming, with both object-oriented and functional features. In the years since, the functional side of Ruby has steadily expanded and matured. In today’s episode, guest chef Devon Estes will show you how to use some features added in Ruby 2.6 to quickly and concisely compose functions together. Enjoy!

Video transcript & code

Function Composition with Ruby 2.6

Ruby is commonly thought of as an Object Oriented language, and in fact people like to say that in Ruby, "everything is an object." But did you know there is one thing in Ruby that actually isn't an object?

When we look at the C implementation for MRI, we find that the humble block is actually the only named concept in Ruby that isn't implemented as an object, and that's for good reason. That's because a block is actually a function.

It turns out that Ruby actually has spectacular support for some of the crucial building blocks of functional programming; most importantly, support for a concept called function composition.

When we pass a block to a method, like here in Array#map, we're using that block as a function that is applied to each element in that array. That block is clearly not an object - it has no methods and no state. But, there is a way to turn a block into an object which you may have seen before.


[1, 2, 3].map { |num| num * 2 }

This 'apply_map' method does the exact same thing as before, but with a bit more indirection. Here we are "capturing" the block passed to the method and then applying it to Array#map.


[1, 2, 3].map { |num| num * 2 }

def apply_map(array, &block)
  array.map(&block)
end

apply_map([1,2,3]) { |num| num * 2 }
# => [2, 4, 6]

But in reality what we're doing here is taking that block and turning it into an instance of the Proc class.


[1, 2, 3].map { |num| num * 2 }

def apply_map(array, &block)
  block.class # => Proc
  array.map(&block)
end

apply_map([1,2,3]) { |num| num * 2 }
# => [2, 4, 6]

Procs are objects that behave like functions, but because they're objects, they have some really interesting methods that we can call that give us a lot of powerful tools to use.

We can also create an instance of the Proc class in several other ways.


[1, 2, 3].map { |num| num * 2 }

def apply_map(array, &block)
  array.map { |num| block.call(num) }
end

apply_map([1,2,3]) { |num| num * 2 }

times_two = Proc.new() { |num| num * 2 }

We can explicitly create a new proc using Proc.new().

There's also a shorthand global method called proc. We'll use this syntax for the rest of this episode.

When we want to actually execute the function that the Proc represents, we send it the call message.


[1, 2, 3].map { |num| num * 2 }

def apply_map(array, &block)
  array.map { |num| block.call(num) }
end

apply_map([1,2,3]) { |num| num * 2 }

times_two = Proc.new() { |num| num * 2 }
times_two = proc { |num| num * 2 }
times_two.call(3)

Now, what if we need to execute a couple of these functions in some sequence. Let's imagine that we're calculating a subtotal for a shop somewhere. To get the subtotal, we need to add some tax. We'll create a simple Proc for that.


total = 10.20
add_tax = proc { |total| total * 1.06 }
subtotal = add_tax.call(total)

We also sometimes need to apply some discounts for our customers with coupons.

For now, we're told that we apply the discount after adding the tax.


total = 10.20
add_tax = Proc.new() { |total| total * 1.06 }
subtract_discount = Proc.new() { |total| total * 0.9 }
subtotal = subtract_discount.call(add_tax.call(total))

What we have here works, but it's not really nice to work with. For a few reasons:

  • In order to understand the order of operations, we effectively have to read it right-to-left
  • There's a sort of false hierarchy here: adding tax is visually buried within substracting the discount

Instead of doing these calculations inline, we could extract a new concept called calculate_subtotal by making a proc that combines the two.


total = 10.20
add_tax = Proc.new() { |total| total * 1.06 }
subtract_discount = proc {|total| total * 0.9 }
calculate_subtotal = proc {|total|  subtract_discount.call(add_tax.call(total)) }
calculate_subtotal.call(total)

Combining two functions in this fashion is known as function composition.

This is such an important technique that, starting with Ruby 2.6, we have built-in support for function composition.

Instead of explicitly creating a third proc, we can directly compose these two functions into a third using the compose-to-right operator.


total = 10.20
add_tax = proc { |total| total * 1.06 }
subtract_discount = proc { |total| total * 0.9 }
calculate_subtotal = add_tax >> subtract_discount
calculate_subtotal.call(total)

The direction the operator is "pointed" visually indicates that arguments will flow through the add_tax function followed by the subtract_discount function. We could also have the data flow the opposite direction. For example...


total = 10.20
add_tax = proc { |total| total * 1.06 }
subtract_discount = proc { |total| total * 0.9 }
calculate_subtotal = add_tax << subtract_discount calculate_subtotal.call(total) # => 9.7308

We just change the direction of that operator, and we now apply the discount before we add the tax.

Now of course these are elementary examples. But the power of function composition is vast, and if this style of programming is one that you like, then Ruby offers many different ways to help do this sort of thing really well.

Responses