Ruby: The Symbol.to_proc coercion

Ruby: The Symbol.to_proc coercion

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:

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_proc1.

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:

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:

And the results:

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:

  1. “Symbol.” Class: Symbol (Ruby 2.4.3), ruby-doc.org/core-2.4.3/Symbol.html#method-i-to_proc.
  2. “Chapter 23: Duck Typing.” Programming Ruby, by David Thomas et al., Pragmatic, 2009.
  3. “Benchmark.” Module: Benchmark (Ruby 2.4.3), ruby-doc.org/stdlib-2.4.3/libdoc/benchmark/rdoc/Benchmark.html#method-c-bmbm.
No Comments

Sorry, the comment form is closed at this time.