Understanding Ruby's idiom: array.map(&:method)
Ruby has some idioms that are used pretty commonly, but not very often understood. array.map(&:method_name)
is one of them.
We can see it being used everywhere to call a method on every array
element, but why does this work? What’s really happening under the hood?
In case you don’t know Ruby’s map
map
is used to execute a block of code for each element of a given Enumerable
object, like an Array
. Here’s an example:
class Foo
def method_name
puts "method called for #{object_id}"
end
end
[Foo.new, Foo.new].map do |element|
element.method_name
end
# => method called for 70339841711300
# => method called for 70339841711280
As we are just calling method_name
for each element of the list, Ruby allows us to use this idiom:
[Foo.new, Foo.new].map(&:method_name)
What Ruby does when it sees &
The first thing that happens is that, whenever Ruby sees a &
for a parameter, it wants this parameter to be a Proc
. If this is not the case already, Ruby calls #to_proc
on this
object to convert it. Let’s confirm this is true:
class MyClass
def to_proc
puts "trying to convert to a proc"
Proc.new {}
end
end
[].map(&MyClass.new)
# => trying to convert to a proc
If you don’t know what a
Proc
is, you can consider it to be just like alambda
or aclosure
. It’s a piece of code that can be moved around and executed (by callingcall()
on it, for instance).
As we passed a MyClass
instance with &
to map
, it tried to call to_proc
on it. This holds true for any method call, not just map
.
Back to the previous example, we are calling map
with &:method_name
. So we know that Ruby will see that &
and try to call :method_name.to_proc
. The next step
is to understand what Symbol#to_proc
does.
Symbol’s smart to_proc
implementation
What Symbol#to_proc
does is quite clever. It tries to calls a method with the same name (in our example, method_name
) on the given object.
Maybe an example will make more sense:
:upcase.to_proc.call("string")
# => STRING
When we call to_proc
on the :upcase
symbol, it will return a Proc
object that just call the upcase
method for the given parameter (“string”).
Implementing our own version
One of the approaches that I like to take to understand how something works is to create my own dumb implementation of it. After we understand all the building blocks that make this idiom work, this should not be that hard.
First, let’s implement our own map
method:
def my_map(enumerable, &block)
result = []
enumerable.each { |element| result << block.call(element) }
result
end
We iterate over the Enumerable
object and execute that given block. We know that block
is going to be a Proc
, because Ruby called to_proc
on it, so we can just call
it.
And this works.
my_map(["foo", "bar"], &:upcase)
# => ["FOO", "BAR"]
Now let’s implement our own Symbol
functionality:
class MySymbol
def initialize(method_name)
@method_name = method_name
end
def to_proc
Proc.new do |element|
element.send(@method_name)
end
end
end
We know that we just need to implement the to_proc
method that Ruby is going to call and make it return a Proc
object.
As this is not really a Symbol
, we will define the method to be called in the constructor. The method name is dynamic, so we
need to use Ruby’s send
to call it.
And this works.
my_map(["foo", "bar"], &MySymbol.new("upcase"))
# => ["FOO", "BAR"]
Summarizing
- Ruby instantiates a
MySymbol
object; - Ruby checks that there is a
&
and callsto_proc
on this object; MySymbol#to_proc
returns aProc
object, that expects a parameter (element
) and calls a method on it (upcase
);my_map
iterates over the received list (['foo', 'bar']
) and calls the receivedProc
on each element, passing it as a parameter (block.call(element)
);- The
Proc
then executeselement.send("upcase")
, that is basically the same as"foo".upcase
, and will return the expected result.
Interested in learning Kubernetes?
I just published a new book called Kubernetes in Practice, you can use the discount code blog to get 10% off.