In on our day to day in Ruby it is very common to apply a method to every object in a collection. Suppose array
is a collection of String objects. We are used to write array.map { |elem| elem.upcase }
. With this code you invoke map on the array object with a block, the map method will yield each element in the collection to the block, which in turn will execute the method upcase
on the element. But what about the more concise array.map(&:upcase)
? It has the same result, but it achieves it in a different way. Have you ever thought about how this internally works? The answer is coercion.
When you tell Ruby array.map(&proc)
you are asking Ruby to pass the Proc
object to the map method as a block. Lets define a Proc
object and apply it to our map function:
1 2 3 4 5 |
my_proc = Proc.new { |elem| elem.upcase } animals = %w{ant bee cat dog eagle frog girafe horse} p animals.map(&my_proc) |
1 2 |
2.4.3 $ ruby my_proc.rb ["ANT", "BEE", "CAT", "DOG", "EAGLE", "FROG", "GIRAFE", "HORSE"] |
It works exactly like a block would. But what happens if what we pass in isn’t already a Proc
object? The answer is that Ruby will try to coerce it by sending it the to_proc
message, and yes, as you expected the Ruby Symbol class coincidentally implements to_proc
1.
You may say: “But Alberto, how can we be sure of that? I want an example!” To see it in action, I have written a class called MySymbol
below this lines, which only holds a symbol value in its object’s state and implements the to_proc
method in a similar way as the Ruby Symbol class would do, that way we will be able to instantiate a MySymbol
object and pass it as we would do with a block. Lets see it in action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class MySymbol def initialize(value) @value = value end def to_proc proc { |obj, *args| obj.public_send(@value, *args) } end end animals = %w{ant bee cat dog eagle frog girafe horse} my_upcase_symbol = MySymbol.new(:upcase) p animals.map(&my_upcase_symbol) |
1 2 |
2.4.3 $ ruby my_symbol.rb ["ANT", "BEE", "CAT", "DOG", "EAGLE", "FROG", "GIRAFE", "HORSE"] |
It works! Now you know how array.map(&:upcase)
does its magic and how you can do the same with your own implemented objects.
Benchmarking
When I read about Symbol#to_proc
coercion in Programming Ruby’s book by Dave Thomas2, it stated: “the use of dynamic method invocations mean that the version of our code that uses &:upcase is about half as fast as the more explicitly coded block”. As that fantastic book was written for Ruby 2.0, lets write a Benchmark to see if that statement still holds true for Ruby 2.4.3:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
require 'benchmark' animals = %w{ant bee cat dog eagle frog girafe horse} * 100_000 Benchmark.bmbm(12) do |bm| bm.report("&:upcase") do animals.map(&:upcase) end bm.report("String#upcase") do animals.map { |animal| animal.upcase } end end |
And the results:
1 2 3 4 5 6 7 8 |
Rehearsal ------------------------------------------------- &:upcase 0.170000 0.020000 0.190000 ( 0.182880) String#upcase 0.140000 0.020000 0.160000 ( 0.160640) ---------------------------------------- total: 0.350000sec user system total real &:upcase 0.090000 0.000000 0.090000 ( 0.085030) String#upcase 0.110000 0.050000 0.160000 ( 0.176765) |
We will ignore Rehearsal times above because they are measured when the Benchmark module is allocating the objects in memory and with Garbage Collection at work, we will focus on the part below instead, which contains the execution times after Benchmark.bmbm
has achieved a stable environment3. Surprisingly the results tell us that the more concise &:upcase
form is now faster than the one with the more explicit coded block! I guess some optimization has been done in the Ruby interpreter to favor the &:upcase
form as it was the preferred form for most Rubyists (including myself). Great!
Remember to share this post if you liked it. In case you want to learn more about this topic I leave you below the bibliography I used to write this post. See you next week!
Bibliography:
- “Symbol.” Class: Symbol (Ruby 2.4.3), ruby-doc.org/core-2.4.3/Symbol.html#method-i-to_proc.
- “Chapter 23: Duck Typing.” Programming Ruby, by David Thomas et al., Pragmatic, 2009.
- “Benchmark.” Module: Benchmark (Ruby 2.4.3), ruby-doc.org/stdlib-2.4.3/libdoc/benchmark/rdoc/Benchmark.html#method-c-bmbm.