Proctology
In this chapter, we look at procs and lambdas, both of which come from the Proc class. We look at the Procs class first and explore multiple ways of calling Procs.
Ever wondered how ["o","h","a","i"].map(&:upcase) expands to ["o","h","a","i"].map { |c| c.upcase) }? We will learn how this is implemented in this chapter!
Next, we examine the difference between a proc and a lambda. If you were ever confused between these two, this section is for you.
We then look at currying, a functional programming concept. Although its practical uses (with respect to Ruby programming) are pretty limited, it is still a fun topic to explore.
Procs and blocks are closely related. In fact, be prepared to see some un-Ruby-like code! Procs and lambdas are ubiquitous in Ruby code. Here, we will see some real-world code that makes good use of procs and lambdas.
Hopefully by then procs and lambdas do not seem mysterious anymore, and a tool that you can readily use at your disposal.
How does Symbol#to_proc work?
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:
words.map &:length
Let’s unravel this syntactical sleight of hand, by figuring out how this works.
What does the &:symbol do?
How does Ruby even 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. We can 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.
Reimplementing Symbol#to_proc
Let’s see what this means. Let’s create an object, and plop it into each:
obj = Object.new => #<Object:0x007ff4218761b8>
[1,2,3].map &obj
TypeError: wrong argument type Object (expected Proc)
That’s awesome! Our error message is telling us exactly what we need to know: it’s saying that obj is well, an Object and not a Proc. The fix is simple: 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
some_obj = Object.new
[1, 2, 3].map &obj #=> [nil, nil, nil]
Now when we run this again, we get no errors. Almost there! How can we then access each element, and say, print it? We need to let out proc accept a parameter:
class Object
def to_proc
proc { |x| "Here's #{x}!" }
end
end
some_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 we know, and redefine to_proc:
class Symbol
def to_proc
proc { |obj| obj }
end
end
We know that in an expression such as
words.map &:length
is equivalent to
words.map { |w| w.length }
Here, the symbol instance is :length. This value of the symbol corresponds to the name of the method. We have previously found out how to access each yielded object – by making the proc return value in to_proc take in an argument.
We want to achieve this effect:
class Symbol
def to_proc
proc { |obj| obj.length }
end
end
How can we use the name of the symbol to call a method on obj? send to the rescue! I hereby present you our own implementation of Symbol#to_proc:
class Symbol
def to_proc
proc { |obj| obj.send(self) }
end
end
Here, self is the symbol object (:length in our example), which is exactly what #send expects.
Improving on our Symbol#to_proc
Our initial implementation of Symbol#to_proc is naïve. The reason is that we only consider the obj in the body of the proc, and totally ignore its arguments.
Recall that unlike lambdas, procs are more relaxed when it comes to the number of arguments it is given. We can therefore easily expose this limitation.
First, we return a lambda instead of a proc in to_proc. Recall that a lambda is a proc, so everything should work as per 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 we know 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 reduce 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'
...
We can now clearly see that we are missing an argument. The lambda currently accepts only 1 argument, but what it received was 2 arguments. We need to allow the lambda to take in 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! We use the splat operator (that’s the * in *args) to support a variable number of arguments. We have one problem though. This doesn’t work anymore:
words = %w(underwear should be worn on the inside)
words.map &:length # => [9, 6, 2, 4, 2, 3, 6]
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, we can give args 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
Or, we can just make it 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 rare cases when being less picky about arity helps. Now that you know how Symbol#to_proc works, it’s time to work on the exercises!
Exercises
- Reimplement Symbol#to_proc: Now that you have seen how
Symbol#to_procis implemented, you should have a go at it yourself. - Class initialisation with #to_proc:
Consider this behavior:
class SpiceGirl
def initialize(name, nick)
@name = name
@nick = nick
end
def inspect
"#{@name} (#{@nick} Spice)"
end
end
spice_girls = [["Mel B", "Scary"], ["Mel C", "Sporty"],
["Emma B", "Baby"], ["Geri H", "Ginger",], ["Vic B", "Posh"]]
p spice_girls.map(&SpiceGirl)
returns:
[Mel B (Scary Spice), Mel C (Sporty Spice),
Emma B (Baby Spice), Geri H (Ginger Spice), Vic B (Posh Spice)]
This example demonstrates how to_proc can be used to initialize a class. Implement this!
Solutions
- The answer is exactly the one in the chapter:
class Symbol
def to_proc
proc { |obj, args| obj.send(self, *args) }
end
end
- Since we are interested in adding behavior to object initialization , it therefore makes sense to implement
to_procwithin theClassclass. Here’s a possible implementation:
class Class
def to_proc
proc { |args| new(*args) }
end
end
Since we are creating objects with arrays, each array element is treated as a single object, therefore the proc takes a single argument.
Next, we use the splat operator to convert the array into method arguments, and pass it into new, which then calls the initializer.