Skip to content

Instantly share code, notes, and snippets.

@zlw5009
Last active June 27, 2021 06:59
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save zlw5009/416d7d52ad90c15f83b17b8103883a64 to your computer and use it in GitHub Desktop.
Save zlw5009/416d7d52ad90c15f83b17b8103883a64 to your computer and use it in GitHub Desktop.
An article on Ruby Blocks and Procs

Building a Foundation with &blocks

What is a Block?

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.

Block or Proc - What will you decide?

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 retaining 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.

Methods that take Blocks

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 is 1. 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.

@dhartoto
Copy link

Great explanation!

@letladi
Copy link

letladi commented Aug 13, 2016

This is great stuff Zach, you're just missing a closing parenthesis in your map method yield statement.

@lamagnotti
Copy link

Awesome!

@montethinks
Copy link

This is really helpful! I haven't looked at Procs since I've been in 101 but now it makes perfect sense thanks to your article. I have one suggested change:

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 retaining access to those local variables. Take the following code for example:

Cheers!

@zlw5009
Copy link
Author

zlw5009 commented Sep 20, 2016

Thanks for the comments everyone. I guess I'm not getting updates on when comments are made so sorry for the delay in response. I'll make the suggested changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment