6 Making Ruby More Functional
Up to now we’ve focused on providing patterns and tools, inspired by functional languages, that can make your existing Ruby code better. The rationale is simple : the more tools you have up your sleeve, the more likely you will have the right one handy when a new problem presents itself.
We’ve shown that Ruby has all it needs to be able to program in a functional style, but to be quite honest its syntax didn’t always cooperate nicely. Inspired by LISP as it may be, its syntax is still optimized for a more traditional, imperative style of programming.
Oh sure, there’s this one neat trick where passing a single inline lambda into a function can be written very elegantly. But when we already have the lambda (or any Procable object) at hand we now need to insert an extra ampersand. It’s an irksome lack of symmetry.
Another annoyance is Ruby’s optional parentheses when calling methods. Well the not the optional parentheses an sich, I’m as happy to drop them in my code as the next Rubyist, but it means we need to do extra work to distinguish between referencing a method (method(:foo)), versus calling a method (foo). Contrast this with languages like Javascript or Python, where this distinction is made through the presence (foo()) or absence (foo) of parentheses. It also introduces asymmetry when calling lambdas (foo.()) vs methods (foo()).
But Ruby isn’t really at fault here. For one language designers optimize for what they consider to be the most common case, especially when it comes to syntax. If Ruby’s creators hadn’t had strong opinions it wouldn’t have been the Ruby we know and love. But also, and here’s the kicker, there’s nothing here we cannot fix ourselves.
In this chapter we will do whatever it takes to make Ruby a beautiful functional language. In the process we’ll be monkey patching a lot of core classes. Let’s get started, it’s time to break the rules!
6.1 Block and Lambda Arguments
When defining a method that takes a lambda as one of its arguments, we typically use a block argument. This way it’s easy to define the lambda inline where it is used.
# Example of a function that takes a single lambda
# Function decorator that negates the result, in other words,
# given a predicate function, it returns the complement.
def not(&prc)
->(*args) {
!prc.(*args)
}
end
# Call the method using an inline block
not_false = not do
false
end
not_false.() # => true
array = []
is_empty = array.method(:empty?)
# Call the method passing in a Procable, the ampersand is necessary
not_empty = not(&is_empty)
not_empty.() # => false
array << 1
not_empty.() # => true
We can see that Ruby syntax is optimized for methods that take a single lambda, which is defined in-line. When a function expects multiple lambdas, or will usually be called with lambdas that were already defined beforehand, then it makes more sense to simply pass in the lambdas as regular arguments.
# Example of a function that takes multiple lambdas
# Juxtapose several lambdas, the result is a single lambda that calls
# all given lambdas in turn with the same arguments, and returns an
# array of the results
def juxt(*lambdas)
->(*args) do
lambdas.map {|l| l.(*args) }
end
end
conversions = juxt(
->(x) { x.upcase },
->(x) { x.capitalize },
->(x) { x.downcase }
)
conversions.('hAppY HaPPy')
# => ["HAPPY HAPPY", "Happy happy", "happy happy"]
6.2 Converting to Proc
There is an extra advantage to using a block parameter, with the explicit ampersand, as opposed to simply passing in lambda values. We get the ability to pass something in that is not a Proc, but could be one.
What I’m talking about is implicit conversion, it’s what allows us to write code like:
[1, 2, 3].map(&:next) # => [2, 3, 4]
The :next in this code is a Symbol, it’s definitely not a Proc. For example we can’t call it directly:
:next.(2)
# NoMethodError: undefined method `call' for :next:Symbol
But a symbol knows how to convert itself to a Proc by implementing the to_proc conversion protocol. When Ruby expects a Proc but gets something else, it will try to call to_proc and hopefully get a Proc that way. We call objects that implement to_proc Procable.
next_proc = :next.to_proc
next_proc.(2) # => 3
Here is how Symbol#to_proc would look like if it were written in plain Ruby (the actual implementation is written in C).
class Symbol
def to_proc
->(*args) { args.first.send(self, *args.drop(1)) }
end
end
These conversion methods are a common theme in Ruby, other examples are to_ary, to_str, and to_path for objects that can be converted to Array, String and Pathname respectively. You don’t usually come across calls to these functions because they happen behind the scenes, that is why they are called implicit conversions. When a core method expects a String you can pass it anything that responds to to_str. Ruby will take the hint and do the conversion for you before proceeding.
It’s a good idea to follow this pattern in our own code. Let’s revisit the juxt method. At the moment we can’t do this:
conversions = juxt(
:upcase,
:capitalize,
:downcase
)
conversions.('hAppY HaPPy')
# NoMethodError: undefined method `call' for :upcase:Symbol
We could give juxt a hand by turning the symbols into procs explicitly:
conversions = juxt(
:upcase.to_proc,
:capitalize.to_proc,
:downcase.to_proc
)
Or just push that conversion into the juxt method itself:
def juxt(*lambdas)
->(*args) do
# was: lambdas.map {|l| l.(*args) }
lambdas.map {|l| l.to_proc.(*args) }
end
end
juxt(:upcase, :capitalize, :downcase).('fEEls gOOd')
# => ["FEELS GOOD", "Feels good", "feels good"]
Related to the implicit (to_int, to_str) and explicit (to_i, to_s) conversion protocols are a set of capitalized conversion methods. Let me quickly demonstrate a few:
Integer("7") # => 7
Integer(5.3) # => 5
String(:foo) # => "foo"
String(99) # => "99"
Array(nil) # => []
Array(3) # => [3]
Array([3]) # => [3]
These will try to convert whatever you pass them to the class with the same name, or fail if they can’t find a meaningful conversion. Unfortunately Ruby lacks a Proc() conversion method, so before we proceed let’s add one:
def Proc(prc)
# Instances of Proc also respond to to_proc, returning themselves
if prc.respond_to?(:to_proc)
prc.to_proc
elsif prc.respond_to?(:call)
prc.method(:call).to_proc
else
raise ArgumentError, "invalid value for Proc(): #{prc.inspect}"
end
end
And with that we arrive at our final, robust, and flexible version of juxt:
def juxt(*lambdas)
->(*args) do
lambdas.map {|l| Proc(l).(*args) }
end
end
6.3 Dropping the Ampersands
One of the most common higher-order functions is Array#map. It only takes a single block parameter, nothing else. So if we want to pass it a lambda, we have to prefix it with an ampersand to make it clear we are passing it in as the block. Let’s fix that so Array#map can handle both cases.
class Array
# First create an alias of the original map, so we can refer to it
alias map_orig map
# Now we can redefine map
def map(lambda = nil, &block)
map_orig(& block_given? ? block : Proc(lambda))
end
end
# Look ma, no ampersands!
[1, 2, 3].map(:next)
We can do the same with each, select, and reject. We are unlikely to break existing code, since the existing version raises an error when called with a regular argument.
Since we are starting to repeat ourselves, let’s whip out some meta-programming to make our lifes easier.
class Module
def autoblock(*args)
args.each do |name|
orig = "#{name}_orig"
alias_method orig, name
define_method name do |lambda = nil, &block|
send(orig, & block ? block : Proc(lambda))
end
end
end
end
This is not a book on meta-programming, so I won’t explain this snippet in detail, but if you squint you can see that it’s simply a generalization of what we did before. Now we can start marking methods as having an “autoblock”:
class Array
autoblock :each, :reject, :select, :map
end
[1,2,3].reject {|x| x > 1} # => [1]
[1,2,3].map(:next) # => [2, 3, 4]
And of course we should use autoblock in our own code. From Ruby 2.1 onwards method definitions (def statements) return the name of the method being defined, so we can write:
# Ruby 2.0 and earlier
def my_method(&blk)
# ...
end
# autoblock must be called after the method has been defined
autoblock :my_method
# Ruby 2.1 and later
autoblock def my_method(&blk)
# ...
end
6.4 Function Composition
Functional programming really comes into its own when you can start “computing” functions. After all functions, lambdas, are really just values. While it might at first glance not make much sense to multiply and subtract one lambda from another, there sure are meaningful operations we can do to “calculate” new lambdas based on existing ones.
We will introduce a few operations by defining operators on Proc and Symbol. Again breaking existing code is unlikely, since these operators don’t exist in vanilla Ruby. However code might rely on these methods not being implemented. 6
module FunOperators
def *(other)
->(*args) { self.to_proc.(other.to_proc.(*args)) }
end
end
[Proc, Symbol].each {|klz| klz.send(:include, FunOperators) }
This * is the compose operator, it takes two lambdas, and pipes the output of one into the other. 7 You can read it as “comes after” or sometimes conveniently as “of the”.
For example, using the Twitter gem:
twitter.home_timeline.select(not(:empty?) * urls).map(
:expanded_url * :first * :urls
)
This snippet selects all tweets in the users home timeline that have one or more urls in them. It then takes the epanded_url of the first of the urls.