A quickish take on Blocks in Ruby
I am currently enrolled in the Launch School program as I transition to a career in software development. What follows is based on what I have learned in the curriculum so far, as it pertains to blocks.
Even though it is said that in Ruby everything is an object, there are a few exceptions. Blocks are one of those exceptions that can be very helpful as they give flexibility and added functionality to our Ruby programs because of how they work. This article aims to explore them a little more in depth.
I will cover the following features of blocks in Ruby:
- Yield keyword
- Block parameters
- Closures
- Explicitly passing a block to a method
Yield:
Blocks are created by placing code (expressions and/or statements) between do..end
or curly braces {..}
. Blocks can be passed into methods implicitly as an argument upon method invocation. On a related and important side note, all methods in Ruby can take an implicit block argument. And by implicit we mean that the block is an optional argument and it is up to the method to either execute the code inside the block or ignore it altogether. One way I like to think of that last statement is that Ruby will not complain that there is a “random” extra argument (in this case the actual block) being passed into the method, upon invocation.
Take for example the following code snippet:
1.times do
puts 'Hi!'
end
Above we have an integer 1
calling the times
method followed by a do..end
block with the expression puts 'Hi!'
inside the block. The block is the argument being passed into the times
method and the code inside the block puts 'Hi!'
gets executed upon invocation of the times
method. Yes, that last statement is correct. The piece of code between the do..end
keywords is an actual argument that is passed into the times
method. Upon invocation, the snippet above will output the string Hi!
one time as that is the way the Integer#times
method is implemented.
But how exactly does a method know to execute the block and when to ignore it? What sort of magic is happening here? The answer lies in the yield
keyword. yield
does exactly that, it will yield the execution of the method to the block that was passed into it as an argument when the method was invoked.
Let’s explore that by defining a custom times
method that works like Ruby’s built in Integer#times
method so that we can visualize what’s going on behind the scenes. Now, in the Ruby documentation we are told that the times
method iterates over the given block the same number of times as the calling object. But the counting begins at the number 0 and ends the iteration at the value of 1 minus the calling object. ( times
is an instance method from the Integer
class so the calling object has to be an integer) We can create our custom times
method using a while loop
to iterate based on the original method explanation in the Ruby documentation.
def times(number)
counter = 0 while counter < number
yield
counter += 1
end number
endtimes(1) { puts "Hi!" } # => Hi!
The method above works almost the same as the Integer#times
which I used in the initial example (The almost part I will explain shortly) As you can see, there is a lot going on inside the method. We have a variablecounter
to keep track of the number of iterations and have set its initial value to 0
to duplivate how Integer#times
works. We then used a while
loop that will check to see if the value of counter
is less than the value referenced bynumber
. Once that is no longer the case, the loop will be exited and the method returns the original argument referenced by number
. This last detail is also another feature of Integer#times
. But most importantly for this discussion, inside the while
loop we use the yield
keyword which yields to the block. This is where the “magic” happens. yield
tells Ruby to look for a block and execute the code inside the block. I like to think of this as the method taking a time out and letting the block do its work before continuing its own job. The value that yield
returns is the return value of the block which is the last expression in the block.
When we invoke our times
method above, we pass in the integer 1
as an argument which is now being referenced by number
inside the method body. The block { puts "Hi!" }
is an additional implicit argument passed into times
. Once the method starts to execute the code, inside the while
loop, it sees yield
and Ruby says, “Let’s see if there’s a block around here…”, it finds the block argument that was passed intotimes
at invocation and executes the code in the the block (in this caseputs "Hi!"
) outputting the string Hi!
and voila. Just like magic right?
Let’s call our custom times
method one more time to expand on yield
some more. Now let’s try calling our times
method without an implicit block to see if it is truly an optional argument.
def times(number)
counter = 0while counter < number
yield
counter += 1
endnumber
endtimes(1) # => (LocalJumpError) no block given
Ooopss…. We get an error this time telling us that we did not pass in a block. How can that be if the block is supposed to be optional? The answer lies once again in the Ruby documentation and how yield
works. The definition states the following:
yield
Called from inside a method body, yields control to the code block (if any) supplied as part of the method call. If no code block has been supplied, calling
yield
raises an exception.
So adding the keyword yield
actually tells Ruby to look for a block and if it does not find one Ruby will complain and let you know. So maybe there’s added magic here that we are not aware of? How does the Integer#times
method ignore the invocation if no block argument has been supplied? This is why the custom times
method above “almost” works like the Integer#times
method. We are missing a key feature.
There is another handy built in method from the Kernel
module Kernel#block_given?
which we can add as a guard clause and helps the method ignore yield
if no block has been provided at invocation. So adding that piece of information makes the final code for our custom times
method look like this:
def times(number)
counter = 0while counter < number
yield if block_given?
counter += 1
endnumber
endtimes(1) { puts "Hi!" } # => Hi!
p times(1) # => 1
Now our custom times
method works just like Ruby’s built in Integer#times
Block parameters:
With blocks we can set parameters (called block parameters) and pass arguments to them through yield
. What’s cool (and sometimes tricky) about this is that the blocks don’t really enforce argument counts like normal methods do. Because of this, blocks are said to have lenient arity rules. Let’s create a new custom method to see this in action:
def a_method
yield(1)
enda_method { |number| puts number } # => 1
In the code above we have set a block parameter |number|
which becomes a local variable inside the block passed into puts
as an argument. Inside a_method
the yield
keyword is passed in an argument 1
in between the parenthesis which it will be referenced by number
upon execution of the block. This is why the code above outputs 1
But what if we don’t pass in an argument to yield
? Will we get an error? Let’s try it:
def a_method
yield
enda_method { |number| p number } # => nil
Nope, no error here and this is because of the arity of blocks I mentioned earlier. Remember that blocks do not enforce argument counts. What happens instead is that the variables inside the block are assigned a value of nil
which is what is output above upon execution. This works the other way around as well. If we passed more arguments to yield
than there are block parameters, the code will still work and the extra arguments will simply be ignored:
def a_method
yield(1, 2, 3)
end
a_method { |number| p number } # => 1
Closures:
Blocks are a way in which Ruby creates closures. Closures are defined as chunks of code that can be saved, passed around and executed. Perhaps more importantly, they are called closures because they create a binding, or they are said to bind its surrounding artifacts and as such they remember the context in which they were defined. This is handy because it means that the block can be called in a different scope yet still maintain access to all the stuff in the scope where the block was created. The best metaphor I remember reading about this is “to think of a block as having a backpack that contains all of the information surrounding it and therefore it has access to that information wherever it goes (I am paraphrasing from a different article that I have been unable to locate again) An example of this would be:
var = 'Hi!'def a_method
yield
enda_method { puts var } # => Hi!
Even though var
was initialized outside the scope of a_method
, the block has access to the value it is referencing because it was created in the same scope as var
. Note that if we initialized var
after the closure was created, the block will not be able to reference its value:
def a_method
yield
enda_method { puts var } # => undefined local variable or method 'var'var = 'Hi!'
Explicitly passing a block to a method:
Blocks can also be passed into a method explicitly. We do this by prepending an ampersand &
to the last parameter in a method definition like this:
def a_method(&block)
block.call
enda_method { puts 'Hi!' } # => Hi!
The reason we say the block is being passed in explicitly is because it is now a parameter in between parenthesis in the method definition. So why would we want to pass a block explicitly you ask? Well if we think of blocks as nameless methods then we can see how that could limit where the block can be used since they are not actual objects. By passing a block explicitly we are giving the method a name that can be passed around to be used in other places just like regular objects. Kind of like we do with objects. This gives us added flexibility in our code.
Notice how we now are using the call
method in the previous example. Why is that? The reason is that by prepending the ampersand &
to the last parameter, this tells Ruby to turn the block into a simple proc
object. A proc
object is an instance of the Proc
class which when created holds a block of code and can be stored in a variable. The call
method is from the Proc
class as we can no longer just use theyield
keyword to yield to the block. But as an added bonus, we can now use the block as an object and pass it around to other methods. We can see this in the following example:
def a_method(block)
p block
enddef another_method(&block)
a_method(block)
endanother_method { "Hi!" } # => <Proc:0x00007fbcd1836658...>
Above we can see that the explicit block passed into another_method
as an argument is converted by Ruby into an object of the Proc
class as is outputted when we call inspect on block
inside a_method
. Notice how we can now pass it without the ampersand to a_method
and use it in there as a regular object. This example is only showing us that it is indeed aproc
object. If we want to execute the code inside the block stored in the proc
we need to use the Proc#call
method as such:
def a_method(block)
puts block.call
enddef another_method(&block)
a_method(block)
endanother_method { "Hi!" } # => Hi!
And that’s it! As you can see, blocks give us added flexibility through all their features. They help make code more reusable by letting us implement general methods that can be fine tuned at invocation by creating more specific code inside blocks that get passed into methods.
I hope you have found this article helpful and good luck in your coding journey!