BrainsToBytes

Ruby iterators and the yield keyword (with examples)

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:

Author image
Budapest, Hungary
Hey there, I'm Juan. A programmer currently living in Budapest. I believe in well-engineered solutions, clean code and sharing knowledge. Thanks for reading, I hope you find my articles useful!