Blocks... What are they anyway? You've probably used them without even realizing it and most certainly have seen them, but do you know what they are?
If we wanted to take a simplistic approach at defining a block we could say:
A block is a chunk of code contained within the
do..end
or { curly braces } syntax that is to be executed at some point in time.
With that being said, if you've ever used the Enumerable#map
or Enumerable#each
method, you've probably used a block. Lets take a look at two different types of blocks before we go into more detail about what a block really is.
A multiline block is usually constructed with the do..end
syntax.
ary = [1, 2, 3]
ary.map do |num|
num * num
end
=> [1, 4, 9]
Without going into explicit detail on what the Enumerable#map
method does, just know that map
will execute the given block once for each element of the array and return a new array with the result returned by the block. With that being said, the multiline block is this chunk of code:
do |num|
num * num
end
Now that we've seen a multiline block, lets take a look at the same code except with an inline block.
ary = [1, 2, 3]
ary.map { |num| num * num }
=> [1, 4, 9]
With this inline block, we can perform the same operation as we performed in the multiline block but with less code. Do note though, it is generally more appropriate to use a multiline block if you're performing a convoluted or long operation.
To refer back to our definition of a block, we stated that a block is a chunk of code that can be executed at some point in time. In the examples shown above, the blocks are passed into the calling method Array#map
as soon as the code is executed. What if we wanted to use that block again? In it's current form, { |num| num * num }
, the block is just a part of the syntax of the map
method call. If we were to write the same block as a standalone block and assign it to a variable, a syntax error will be returned:
a = { |num| num * num }
=> SyntaxError: syntax error, unexpected '|', expecting '}'
This is because a standalone block is not an object and holds no value in memory therefore it can only be provided in the argument list. Another way to show this is by using the Object#class
method.
"This is a string".class
=> String
13.class
=> Fixnum
[1, 2, 3].class
=> Array
{a: 1, b: 2, c: 3}.class
=> Hash
{ |num| num * num }.class
=> SyntaxError: syntax error, unexpected '|', expecting '}'
However, if we were to assign the block to a Proc
object, we could retain the block for future use.
a = Proc.new { |num| num * num }
=> #<Proc:0x007fc3892d4480>
a.class
=> Proc
This is where the terminology "executed at some point in time" from our definition really starts to unfold. By assigning our block to an instance of the Proc
class, we can reuse that "chunk of code". You may be wondering "why not just put that 'chunk of code' in a method" which is a valid question. Methods are great to perform some type of function. However, methods cannot be passed into other methods whereas blocks can. The usability of blocks and availability make them a powerful tool. We will see how to pass a block into a method in just a minute.
When a Proc object is created, the block of code is bound to a set of local variables. Once bound, the block may be utilized in different contexts while remaining access to those local variables. Take the following code for example:
ary = [1, 2, 3]
hash = {a: 1, b: 2, c: 3}
a = Proc.new { |num| num * num }
ary.map(&a)
=> [1, 4, 9]
hash.each_value.map(&a)
=> [1, 4, 9]
By using the &
ampersand sign, we're telling ruby that a
is a Proc object and is to be executed as a block.
As you progress through Ruby, it may be useful to write methods that take blocks. We will walk through a few methods that you're most likely familiar with and explain how to write methods that take blocks as we go.
Before we can understand how to pass blocks into a method, we first must understand how yield works.
def yield_test
puts "You're in the method"
yield
puts "Back in the method"
yield
end
yield_test { puts "You're in the block" }
=> You're in the method
=> You're in the block
=> Back in the method
=> You're in the block
As you can see, the yield statement performs the operation in the block then proceeds within the method after the block has finished it's operations. When we call yield within a method, we're telling Ruby that we want to let the block perform some operation before continuing. We can also pass parameters into a block with yield.
def yield_test
puts "You're in the method"
yield 1
puts "Back in the method"
yield 13
end
yield_test { |parameter| puts "You're in the block: #{parameter}" }
=> You're in the method
=> You're in the block: 1
=> Back in the method
=> You're in the block: 13
The yield statement is written first followed by the parameters that we want to pass into the block. You can pass in multiple parameters as well following the same format that we showed above.
The Array#each
method is a common method to iterate through an array which calls the given block once for each element in the array, passing that element as a parameter into the block for each iteration. After the iteration is complete, the each
method returns the array itself.
Let's look at a brief example of Array#each
first then we will go ahead a write up our own each
method.
a = [1, 2, 3]
a.each { |num| puts num }
1
2
3
=> [1, 2, 3]
As we can see, each element is printed out using puts
which is called within the block then the array that called the each
method is returned after iteration.
Coding our own version of the each
method may look something like this:
def each(array)
element = 0
while element < array.size
yield(array[element])
element += 1
end
array
end
each([1, 2, 3]) { |element| puts element }
1
2
3
=> [1, 2, 3]
Let's break our solution down step by step.
- First we create a variable
element
and set it to 0 - Next, we create a while loop which will continue to loop until the value of
element < array.size
- On line 5, we yield to the block passing a parameter to the block. The parameter we are passing it depends on which iteration we are on. For instance, during the first iteration, we are passing in the element at
array[0]
as a parameter which is1
. On the next iteration,2
is passed to the block as a parameter and so forth. - After the operation is completed by the block, we increment the element by 1.
- We repeat steps 2-4 until the while loop breaks; then we return the original array.
Now we're going to build another method that takes a block and resembles the Array#map
method.
def map(array)
result = []
element = 0
while element < array.size
result << yield(array[element]
element += 1
end
result
end
map([1, 2, 3]) { |element| element * 2 }
=> [2, 4, 6]
Here, we're performing a similar operation as we did in our each method except this time, we're returning a new array called result
which contains the return values from the operation performed within the block during each iteration.