How Symbol#to_proc Works

Symbol#to_proc is one of the finest examples of the flexibility and beauty of Ruby. This syntax sugar allows us to take a statement such as

 words.map { |s| s.length }

and turn it into something more succinct:

 words.map(&​:length​)

Let’s unravel this syntactical sleight of hand by figuring out how this works.

The first step is to figure out the role of the &:symbol. How does Ruby know that it has to call a to_proc method, and why is this only specific to the Symbol class?

When Ruby sees an & and an object—any object—it will try to turn it into a block. This is simply a form of type coercion.

Take to_s, for example. Ruby allows you to do 2.to_s, which returns the string representation of the integer 2. Similarly, to_proc will attempt to turn an object—again, any object—into a Proc.

This might seem a little abstract, so in order to make things more concrete, it’s time to open irb.

Reimplementing Symbol#to_proc

In order to understand what happens behind the scenes, you’ll create an object and then pass it into map. If you’re expecting this to fail, you are absolutely right, but that is the whole point. The error messages that Ruby provides will guide you to enlightenment.

 >>​ obj = Object.new
 >>​ [1,2,3].map &obj
 TypeError: wrong argument type Object (expected Proc)

The error message is telling us exactly what you need to know. It’s saying that obj is, well, an Object and not a Proc. In other words, we must teach the Object class how to turn itself into a Proc. Therefore, the Object class must have a to_proc method that returns a Proc. Let’s do the simplest thing possible:

 >>​ ​class​ Object
 >>​ ​def​ to_proc
 >>​ proc {}
 >>​ ​end
 >>​ ​end
 =>​ ​:to_proc
 
 >>​ obj = Object.new
 
 >>​ [1, 2, 3].map &obj
 =>​ [​nil​, ​nil​, ​nil​]

When you run this again, you’ll get no errors. But notice that the result is an array of nils. How can each element be accessed and, say, printed out? The Proc needs to accept arguments:

 >>​ ​class​ Object
 >>​ ​def​ to_proc
 >>​ proc { |x| ​"Here's ​​#{​x​}​​!"​ }
 >>​ ​end
 >>​ ​end
 =>​ ​:to_proc
 
 >>​ obj = Object.new
 
 >>​ [1,2,3].map(&obj)
 =>​ [​"Here's 1!"​, ​"Here's 2!"​, ​"Here's 3!"​]

This hints at a possible implementation of Symbol#to_proc. Let’s start with what you know and redefine to_proc in the Symbol class:

 >> ​class​ Symbol
 >> ​def​ to_proc
 >> proc { |obj| obj }
 >> ​end
 >> ​end
 => ​:to_proc

You now know that an expression such as

 words.map(&​:length​)

is equivalent to

 words.map { |w| w.length }

Here, the Symbol instance is :length. The value of the symbol corresponds to the name of the method. You also know how to access each yielded object by making the Proc return value in to_proc take in an argument.

For the preceding example, this is what you want to achieve:

 class​ Symbol
 def​ to_proc
  proc { |obj| obj.length }
 end
 end

You can even try this out:

 >>​ ​class​ Symbol
 >>​ ​def​ to_proc
 >>​ proc { |obj| obj.length }
 >>​ ​end
 >>​ ​end
 =>​ ​:to_proc
 
 >>​ [​"symbol"​, ​"cymbals"​, ​"sambal"​].map(&​:obj​)
 =>​ [6, 7, 6]

Unfortunately, this only works on objects that have the length method. How can Symbol#to_proc be made more general?

Well, how can the name of the symbol be turned into a method call on the obj? This can be answered in two parts.

First, using Kernel#send, any method can be invoked on an object dynamically as long as the right symbol is supplied. For example:

 >>​ ​"ohai"​.send(​:length​)
 =>​ 4

In other words, send allows you to dynamically invoke methods using a symbol. In this example, :length is hard-coded in the Symbol#to_proc method. The next step is to make the method more general, which brings us to the next part of the answer.

Instead of hard-coding :length, you can make use of self, which in the case of a Symbol, returns the value of the symbol.

Therefore, you can make use of self, which holds the value of the name of the method such as :length, and pass it to the send method to invoke the method on obj. I hereby present you our own implementation of Symbol#to_proc:

 class​ Symbol
 def​ to_proc
  proc { |obj| obj.send(self) }
 end
 end

Try it out. Save this code to a file called symbol_to_proc.rb and then load it in irb:

 $ ​​irb​​ ​​-r​​ ​​./symbol_to_proc.rb

Then test it out:

 >>​ [​"symbols"​, ​"cymbals"​, ​"sambal"​].map(&​:length​)
 =>​ [7, 7, 6]
 
 >>​ [​"symbols"​, ​"cymbals"​, ​"sambal"​].map(&​:upcase​)
 =>​ [​"SYMBOLS"​, ​"CYMBALS"​, ​"SAMBAL"​]

self is the symbol object (:length in our example), which is exactly what #send expects.

Improving on Symbol#to_proc

The initial implementation of Symbol#to_proc is naïve. The reason is that only the obj in the body of the Proc in considered, while the arguments are totally ignored.

Recall that unlike lambdas, Procs are more relaxed when it comes to the number of arguments they’re given. It’s therefore easy to circumvent this limitation. It’s instructive to see what happens when a lambda is used instead of a Proc.

First, we return a lambda instead of a Proc in to_proc. Recall that a lambda is a Proc, so everything should work as normal:

 class​ Symbol
 def​ to_proc
» lambda { |obj| obj.send(self) }
 end
 end
 
 words = ​%w(underwear should be worn on the inside)
 words.map &​:length​ ​# => [9, 6, 2, 4, 2, 3, 6]

Since lambdas are picky when it comes to the number of arguments, is there a method that requires two arguments? Of course: inject/reduce. The usual way of writing inject is:

 [1, 2, 3].inject(0) { |result, element| result + element } ​# => 6

As you can see, the block in inject takes two arguments. Let’s see how our implementation does by using the &:symbol notation:

 [1, 2, 3].inject(&:+)

Here’s the error we get:

 ArgumentError: wrong number of arguments (2 ​for​ 1)
 from (irb):10​:in​ ​`block in to_proc'
 from (irb):14:in `​each​'
 from (irb):14:in `inject'
 ...

You can now clearly see that an argument is missing. The lambda currently accepts only one argument, but what it received was two arguments. You need to allow the lambda to take in more arguments:

 class​ Symbol
 def​ to_proc
» lambda { |obj, args| obj.send(self, *args) }
 end
 end
 
 [1, 2, 3].inject(&:+) ​# => 6

Now it works as expected! The splat operator (that’s the * in *args) enables the method to support a variable number of arguments. Before you go about celebrating, there’s one problem. The following code doesn’t work anymore:

 words = ​%w(underwear should be worn on the inside)
 words.map &​:length​ ​# => [9, 6, 2, 4, 2, 3, 6]

You’ll see the following output when you run it:

 ArgumentError: wrong number of arguments (1 for 2)
 from (irb):3:in `block in to_proc'
 from (irb):8:in `map'
 ...

There are two ways to fix this. First, you can supply args with a default value:

 class​ Symbol
 def​ to_proc
» lambda { |obj, args=​nil​| obj.send(self, *args) }
 end
 end
 
 words = ​%w(underwear should be worn on the inside)
 words.map &​:length​ ​# => [9, 6, 2, 4, 2, 3, 6]
 
 [1, 2, 3].inject(&:+) ​# => 6

Alternatively, you can just use a Proc again:

 class​ Symbol
 def​ to_proc
» proc { |obj, args| obj.send(self, *args) }
 end
 end
 
 words = ​%w(underwear should be worn on the inside)
 words.map &​:length​ ​# => [9, 6, 2, 4, 2, 3, 6]
 
 [1, 2, 3].inject(&:+) ​# => 6

This is one of the few places where having a more relaxed requirement with respect to arities is important and even required.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.93.64