The Enumerable
module is one of my favorite parts of Ruby. It allows us to
perform operations like #each
, #map
, #inject
, and #any?
on collection
objects, most notably Array
. It’s all much cleaner, readable, and semantic
than the nested for
loops I used to write in other languages.
Enumerable
is made possible by leveraging one of Ruby’s more powerful
constructs: blocks. They typically take the form of:
collection.each { |item| puts item }
or
collection.map do |item|
item * 2
end
More cryptically, you may also see:
collection.all?(&:valid?)
or
collection.inject(:&)
Let’s take a closer look at what’s going on here.
Blocks
Blocks allow methods to take arbitrary snippets of code as arguments and run them. The pattern of passing in an anonymous function into a method is commonly used across multiple languages. JavaScript, for example, is notorious for its use of deeply nested anonymous callback functions.
item.action(function(result) {
result.otherAction(function(newResult) {
console.log(newResult);
});
});
In Ruby, you might express “multiply each element of this collection by 2 and
return the result” by passing the anonymous function n * 2
to the
Enumerable#map
method:
[1, 2, 3].map { |n| n * 2 }
# => [2, 4, 6]
Procs
The concept of passing functions as arguments to other functions is quite
powerful. Methods that have this ability are known as higher order functions.
It is common (particularly in functional languages) to want to pass in a named
function instead of an anonymous one. Ruby allows us to do that with Proc
s.
Proc
is Ruby’s function object. We can pass a proc object instead of a block
to any method by prefixing it with &
. For example we could use the named
function double
in our previous snippet:
double = Proc.new { |n| n * 2 }
[1, 2, 3].map(&double)
# => [2, 4, 6]
#to_proc
Ruby does this clever trick where it doesn’t immediately try to run the Proc
passed into the function but instead calls #to_proc
on it before execution.
This allows us to pass in any object that responds to #to_proc
.
class Doubler
def double(n)
n * 2
end
def to_proc
method(:double).to_proc
end
end
doubler = Doubler.new
[1, 2, 3].map(&doubler)
# => [2, 4, 6]
Symbols
Passing in an object that responds to to_proc
might seem like a cool but
useless party trick that wouldn’t be used in a production codebase. That’s where
Symbol
comes in. It defines #to_proc
as a function that sends the message of
the same name as the symbol to the target object.
For example:
[1,2,3].map(&:to_s)
# => ["1", "2", "3"]
sends the message :to_s
to each item in the array.
To get a better idea of how this works, we can try to replicate this behavior on
String
.
class String
def to_proc
method_name = self # the string is the method name
Proc.new { |item| item.send(method_name) }
end
end
[1, 2, 3].map(&"to_s")
# => ["1", "2", "3"]
This allows us to write terser and more readable code. For example, “are all the
numbers even?” can be expressed as numbers.all?(&:even?)
instead of
numbers.all? { |n| n.even? }
.
Enumerable#inject
inject
(aka reduce
) is one of the most powerful methods provided by
Enumerable
. In fact, all of the other methods provided by Enumerable
could
be written in terms of inject
(This general concept is called catamorphisms.
This paper takes a more detailed look at the topic). However, inject
is
weird. It takes a block just like any other method in Enumerable
. But there’s
a twist. Taking a look at the docs:
Combines all elements of enum by applying a binary operation, specified by a block or a symbol that names a method or operator.
This means that it can accept a symbol instead of a block and that the symbol
will be used to send that message to the objects, just like Symbol#to_proc
.
So you where you would use a traditional block syntax with Symbol#to_proc
:
[1, 2, 3].inject(&:+)
# => 6
you could just use the bare symbol
[1, 2, 3].inject(:+)
# => 6
This can lead to the confusing expression:
[true, false, true].inject(:&)
# => false
At first glance this may look like someone tried to use Symbol#to_proc
but
forgot to fill in the method name. However, note that the &
and :
are
flipped. Symbol#to_proc
uses &:
, not :&
.
What’s actually happening is that inject
is using the method &
to combine
the elements: true & false & true
. The equivalent using Symbol#to_proc
would
be the even stranger looking
[true, false, true].inject(&:&)
# => false