This article includes code samples, check the 'What to do next' section for the link to the repo
Ruby iterators are a special type of method supported by collections. They are like any other regular method, but they receive an additional input in the form of a code block. Iterators are one of the most useful features of the Ruby language, and using them effectively is a great way of creating clean methods and classes.
In this article, we will explore the fundamental concepts behind iterator methods. We will learn how the yield keyword works and write our own replicas of commonly-used iterators found in the language.
In production code, you will rarely (if ever) create your own implementations of these common iterators. Still, building your own versions is a great way of learning this topic in-depth, and will prepare you to create your own iterators if the need arises.
Iterating through code blocks
Iterators are regular methods that receive extra input as part of their calling syntax. This extra input is a code block: a series of regular valid Ruby statements that the iterator can call one or more times. To understand this, let's see an example of one of the simplest iterators, times.
Times is a method in the Integer class that will execute a code block n times, where n is the integer it's called on. The following code will print a message 10 times.
10.times { puts "This message will appear 10 times, yay!"}
In this case, the code between the curly brackets (the puts statement) is the code block. As expected, running this code prints the following result in the terminal:
This message will appear 10 times, yay!
This message will appear 10 times, yay!
This message will appear 10 times, yay!
This message will appear 10 times, yay!
This message will appear 10 times, yay!
This message will appear 10 times, yay!
This message will appear 10 times, yay!
This message will appear 10 times, yay!
This message will appear 10 times, yay!
This message will appear 10 times, yay!
Curly brackets are not the only way to pass a code block, you can also use do/end. The following snippet is equivalent to the one we wrote above, and produces the same results:
10.times do
puts "This message will appear 10 times, yay!"
end
Some iterators can also pass data into a code block. You just need to specify the argument in the code block using '||' symbols. For example, the iterator each will pass into the code block every element in the array it's called on:
test_array = [1, 10, 100, 1000, 10000]
test_array.each do |element|
puts "Nice, the iterator gave me this value: #{element}"
end
If you run this code, it will print this message:
Nice, the iterator gave me this value: 1
Nice, the iterator gave me this value: 10
Nice, the iterator gave me this value: 100
Nice, the iterator gave me this value: 1000
Nice, the iterator gave me this value: 10000
Now that we are familiar with iterators, we will build our own versions using the yield operator.
Writing your own iterators
Writing your own iterators is not a difficult task, all you need to do is to pass code blocks and then move the execution into the code block. In Ruby, this can be achieved by using the yield operator. For understanding this better, take a look at the following example:
def simplest_yielding_method
puts "I am at the simplest yielding method, after this, execution will move into the code block"
yield
puts "I am back at the simplest yielding method"
end
simplest_yielding_method do
puts "Currently executing the code block"
end
In our example, yield will move the execution to whatever you passed as a code block to simplest_yielding_method. When the code block is done running, execution will return and run the statements after the yield call. As expected, if you run this code you will get this result:
I am at the simplest yielding method, after this, execution will move into the code block
Currently executing the code block
I am back at the simplest yielding method
Running a code block infinite times
Let's try a more interesting example: a method called infinite_loop that executes a code block an infinite number of times. The implementation is quite straightforward:
def infinite_loop
while true
yield
end
end
infinite_loop do
puts "This message will appear infinite times"
end
Run this code and you will see the while loop yielding control to the code block over and over again. We can also create a variation of this method that runs only N times. For that, we can use monkey-patching to add a method called run_times to the Integer class:
class Integer
def run_times
counter = 0
until counter == self
yield
counter += 1
end
end
end
5.run_times do
puts "This message will appear exactly 5 times"
end
Running this code results in:
This message will appear exactly 5 times
This message will appear exactly 5 times
This message will appear exactly 5 times
This message will appear exactly 5 times
This message will appear exactly 5 times
We just learned how to move the execution into the code block using the yield keyword. This is by itself quite powerful, but it would be very limited if moving data between the iterator and the code block wasn't possible. We will learn how to do this by implementing a couple of demos.
Moving data to and from a code block
You can pass information into a code block by passing them as arguments for yield. You can receive them in the code block by specifying them when writing the block. Again, this is much easier to understand with an example. Let's create our own version of the Array method each.
class Array
def my_each
counter = 0
until counter == self.size
# We will pass into the code block each element of the array, one by one
yield self[counter]
counter += 1
end
self # Just like the normal each method, we return self at the end
end
end
test_array = [1, 10, 100, 1000, 10000, 100000]
# This is how we specify the arguments passed to the code block
# In this case, we decide to call it 'element', but the names are up to you
test_array.my_each do |element|
puts "The test array contains the number #{element}"
end
In this case, we chose element as the argument name for the block, but you can name them whatever you want.
It's also possible to return data from a code block and use it in the iterator. In this case, the return value of the code block can be seen as the return value of calling the yield function. Again, this is easier to understand with an example.
Let's re-implement the Array.map function. Map returns an array where each entry is the result of applying the block function to the respective element of the original array. Our implementation would be:
class Array
def my_mapper
mapped_array = []
counter = 0
until counter == self.size
mapped_value = yield self[counter]
mapped_array.push(mapped_value)
counter += 1
end
# we want to return the mapped array in the end
mapped_array
end
end
test_array = [1,2,3,4,5,6,7,8,9]
# Let's generate an array with the squares of test_array
squares = test_array.my_mapper do |element|
element * element
end
puts squares
Pay attention to the line where we use yield. The return value of yield is put into the mapped_value variable and pushed into an array. In this example, this new array will be filled with the squares of every element in the original array.
Running this code prints the following result:
1
4
9
16
25
36
49
64
81
Iterators are useful
Now you know how to write your own iterators.
As we mentioned before, you can accomplish most everyday programming tasks with a combination of the built-in iterators. Still, knowing how to leverage the power of yield can lead to elegant solutions and clean methods.
If you still want to dig a bit deeper in the topic try implementing some of the other iterators like select, take_while and inject. I've found that writing your own copies of common iterator functions is the easiest way to master this topic.
What to do next:
- Share this article with friends and colleagues. Thank you for helping me reach people who might find this information useful.
- You can find the code samples for this article here
- The documentation for the enumerable module specifices almost every iterator you'll ever use.
- Check chapter 6 of The Well-Grounded Rubyist for an in-depth take on iterators and other control flow techniques. This and other very helpful books can be found in the recommended reading list.
- Send me an email with questions, comments or suggestions (it's in the About Me page). Come on, don't be shy!