Writing Your Own Enumerables in Ruby
Here’s an Enumerable
:
[1,2,3].each do |item|
puts item
end
We start with an array of items that we want to visit one by one, in this case the numbers 1, 2, and 3.
Calling .each
on our array means that we want to visit all 3 of these numbers. The code block that follows .each
begins with do
and ends with end
. We want this code block to execute not once for the array as a single object, but instead multiple times, once for every item inside of the array.
The example code that we want to execute each time is puts item
. item
is the keyword that we chose to represent whatever thing in the array we’re currently looking at. We defined it immediately after do
by placing it between pipes like so: |item|
So what’s going on here? .each
is tagged onto the end of an array, which makes it look like each
may be a method being called on that array.
Then it seems like we may have two separate code blocks executing: One code block is saying to visit everything in the array, the other code block is defining what to do with each of those items as we’re visiting them.
Let’s rebuild this into our own method. Instead of .each
, we’ll write a custom method called .my_each
[1,2,3].my_each=> undefined method `my_each' for [1, 2, 3]:Array
That makes sense because we haven’t yet defined our method anywhere. Since [1,2,3]
is an instance of the Array class, let’s make our method an instance method on that class.
class Array
def my_each
end
end[1,2,3].my_each=> nil
Before we go forward, let’s get our bearings. When we call my_each
on an array, what is the thing that we’re working with from within the method? In other words, what is self
?
class Array
def my_each
puts "#{self.class}: #{self}"
end
end[1,2,3].my_each=> Array: [1, 2, 3]
It looks like within our method we’re looking at the entire array that we called my_each
on. Since our ultimate goal is not to act on the whole array, but instead to act on the individual components of the array, let’s begin by trying to interact with those individual components. We can do this by iterating through every item in the array and calling puts
for each of those items.
class Array
def my_each
for item in self
puts "#{item}"
end
end
end[1,2,3].my_each=> 1
2
3
This behaves just like the each
method we started with except for one big difference: Our custom method my_each
is hardcoded to always execute the same code, puts "#{item}"
every time the method is called while each
allows the caller to specify what code block they want to run on each iteration of the loop.
[1,2,3].each do |item|
puts item + 10
end=> 11
12
13[1,2,3].my_each do |item|
puts item + 10
end=> 1
2
3
While each
evaluated the passed in code block and used it to affect its output, our method ignored the given code block entirely and acted like it wasn’t even there. This is because even though Ruby methods can receive and act on code blocks passed to them, those methods receiving the blocks must acknowledge that it was passed and allow it to execute.
One way we can do this is with the yield
keyword. Using this keyword says “At the same place I use the yield
keyword, I’d like to allow the passed in code block to execute.” Let’s try it.
class Array
def my_each
yield
end
end[1,2,3].my_each do
puts "Hello"
end=> "Hello"
By giving the code block permission to execute by using the keyword yield
we were able to take a code block passed to our method and execute it just like each
does.
Now let’s get the passed in code block to reference our array, since that’s what we’re ultimately going to want to affect.
class Array
def my_each
yield
end
end[1,2,3].my_each do
puts "#{self}"
end=> main
Interesting. We already confirmed that [1,2,3]
is an instance of Array and that our method my_each
is an instance method on Array, so why would self
be main
instead of [1,2,3]
?
The reason is because the passed in code block is evaluated before the method is called. At the time that the code block executes to find out what it is, self
is main
, so when the method reaches our yield
it puts
to the screen what self
was when it first ran, which is main
.
Alright, so it looks like if we want to reference our array we’ll need to do that from within the method. Somehow we need to let the passed in code block know about the array that we want to operate on. Luckily, yield
can take parameters and these parameters will allow us to talk to the code block.
class Array
def my_each
yield(self)
end
end[1,2,3].my_each do |what_am_i|
puts "#{what_am_i}"
end=> [1,2,3]
We did two important things here. The first thing we did is pass yield
a parameter of self
. Because my_each
is an instance method, we know that from within the method, self
is the instance of the Array, in our case [1,2,3]
.
The second thing we did is give an iterator to the code block that we’re passing in. By adding |what_am_i|
our code block is saying “I’m expecting to have access to a parameter that was passed in. I don’t know what that parameter will be, but I want the keyword what_am_i
to stand in for it so that I can act on it. When yield
passed self
and it turned out that self
was the array, the code block dutifully took that array and printed it to the screen.
We’re just missing one last piece, which is that we want our code block to execute for every item in the array, not for the array itself! To do that we need to call yield
for every item individually.
class Array
def my_each
for item in self
yield(item)
end
end
end
Let’s retry our example from earlier where we were comparing my_each
with each
[1,2,3].each do |item|
puts item + 10
end=> 11
12
13[1,2,3].my_each do |item|
puts item + 10
end=> 11
12
13
my_each
has quickly become a very powerful method that can also serve as a foundation to make more complicated Enumerable
methods, like map
.
Ruby’s map
method, like each
, also takes a code block and performs that code block’s operation on each item in an array. This method however returns a different array, one that’s been transformed by the code block passed to it.
[1,2,3].map do |item|
item * 2
end=> [2,4,6]
The array itself has been changed! We can do this ourselves just by adding a few extra lines of code.
class Array
def my_each
for item in self
yield(item)
end
end def my_map
new_array = []
for item in self
new_array << yield(item)
end
new_array
end
end[1,2,3].my_map do |item|
item * 2
end=> [2,4,6]
Next let’s try to implement a neat shortcut that map
uses when it has a single operation that it wants to perform on every item in the array, which is the syntax that looks like this:
[1,2,3].map(&:even?)=> [false,true,false]
Pretty cool, eh? This syntax says “I want you take the method even?
, call it on every item in the array, and then save that return value to the array in the place of the item that was there.” Calling even?
on an Integer returns true or false, so that true or false return value is what replaces the number in the array. It’s equivalent to:
[1,2,3].map do |item|
item.even?
end
This syntax makes the call much shorter and saves us the trouble of having to define our iterator, which is pretty handy. Let’s try it with my_map
[1,2,3].my_map(&:even?)=> [false,true,false]
Wow! Works right out of the box without us making any changes at all! Here’s why:
When we pass a symbol
as a block parameter instead of passing a block, Ruby automatically calls to_proc
on it, since a proc
is something that can both hold and later execute block code. The symbol
(in our example case :even?
) is turned into a proc
which can then be called (executed) with an additional parameter. This additional parameter will identify what object we want the code block to execute on. That means that this:
for item in [1,2,3]
yield(item)
end
Becomes this:
for item in [1,2,3]
:even?.to_proc.call(item)
end
Which is equivalent to
for item in [1,2,3]
item.even?
end
Neat.
For more practice with custom Enumerables
, try implementing some of the other fun Enumerable
instance methods like any?
, all?
, or select
[1,2,3].any? do |item|
item == 1
end=> true
[1,2,3].all? do |item|
item == 1
end==> false
[1,2,3].select do |item|
item == 1
end==> [1]
Can you improve on these or make your own?