Saturday, April 30, 2016

Ruby Blocks

In Ruby, a block is basically a chunk of code that can be passed to and executed by any method. Blocks are always used with methods, which usually feed data to them (as arguments).

Blocks are widely used in Ruby gems (including Rails) and in well-written Ruby code. They are not objects, hence cannot be assigned to variables.

Basic Syntax


A block is a piece of code enclosed by { } or do..end. By convention, the curly brace syntax should be used for single-line blocks and the do..end syntax should be used for multi-line blocks.

{ # This is a single line block }

do
    # This is a multi-line block
end

Any method can receive a block as an implicit argument. A block is executed by the yield statement within a method. The basic syntax is:

def meditate
    print "Today we will practice zazen"
    yield  # This indicates the method is expecting a block
end

meditate { print " for 40 minutes." }  # We are passing a block as an argument to the meditate method
Output:

Today we will practice zazen for 40 minutes.

When the yield statement is reached, the meditate method yields control to the block, the code within the block is executed and control is returned to the method, which resumes execution immediately following the yield statement.

Below is an example of a multi-line block:

meditate do
  puts " for 40 minutes."
end
Output:

Today we will practice zazen for 40 minutes.

When a method contains a yield statement, it is expecting to receive a block at calling time. If a block is not provided, an exception will be thrown once the yield statement is reached. See example below:

meditate  # Call method without passing a block
Today we will practice zazen.
LocalJumpError: no block given (yield)

We can make the block optional and avoid an exception from being raised:

def meditate
    puts "Today we will practice zazen."
    yield if block_given?
end

meditate
Output:


Today we will practice zazen.

It is not possible to pass multiple blocks to a method. Each method can receive only one block.

Blocks can be useful in many different ways. Below is one example of a method that will measure the execution time of any piece of code.

def time_it
    start_time = Time.now
    yield
    puts "Execution time: #{ Time.now - start_time } secs."
end

time_it { sleep 1 } 
Output:

Execution time: 1.001391551 secs.

Blocks can take arguments


To pass an argument to a block, we have to pass the argument to the yield statement and specify its name at the block definition. The syntax is as follows:
def meditate
    yield("zazen")  # "zazen" is an argument passed from the method to the block
end

When calling the meditate method, we will provide a block that takes an argument called "name". Notice the expected argument is wrapped in | (pipe) characters.

meditate {|name| puts "Today we will practice #{name}."}
Output

Today we will practice zazen.

Blocks can also take multiple arguments, like this:

def meditate
    yield("zazen", 40)  # "zazen" and 40 are arguments passed from the method to the block
end

meditate {|name, time| puts "Today we will practice #{name} for #{time} minutes."}
Output:

Today we will practice zazen for 40 minutes.

Unlike methods and lambdas, blocks handle arguments with great flexibility. It silently defaults missing argument values to nil, discards unexpected (extra) arguments, unpacks arrays of arguments, etc.

Blocks can have return statements


The return statement in a block behaves differently than that of a method (or a lambda). If a method is defined inside another method, the return statement exits only from the inner method itself; then the outer method resumes execution at the following line. In contrast, if a block is defined within a method, a return statement within the block will exit the block itself as well as the enclosing method.

Example:

def meditate
    puts "Adjusting posture…"
    1.times { puts "Ringing bell…"; return }
    puts "Sitting still…"  # This is not executed
end

meditate
Output

Adjusting posture…
Ringing bell…

If we define a block without an enclosing (outer) method and include a return statement, it will throw a LocalJumpError.

1.times { puts "Ringing bell…"; return }
Output

Ringing bell…
LocalJumpError: unexpected return

This happens because when a return statement is reached within a block, instead of returning (exiting) from the method that yielded (called) the block, it will return from the scope on which it (the block) was defined. In the following example, a LocalJumpError happens because the block is trying to return from the top-level context, where it was defined.


def meditate
    puts "Adjusting posture…"
    yield
    puts "Sitting still…"  # This is not executed
end

meditate { puts "Ringing bell…"; return }
Output:

Adjusting posture…
Ringing bell…
LocalJumpError: unexpected return

Usually, it's not a good idea to use a return statement within a block. We can usually come up with different (better) ways to approach the problem. If we really need to return something, it's probably best to use a lambda. Lambdas handle return statements in a different way, more like methods.

If all we need is to exit prematurely from a block, we can use a next statement.

1.times do
    puts "Adjusting posture…"
    next   # Exit block immediately
    puts "Sitting still…"  #This line is never executed
end
Output:

Adjusting posture...

Notice that the line below the next statement did not run.

Another example:

def meditate
    puts "Adjusting posture…"
    1.times { puts "Ringing bell…"; next; "Wreaking havoc at the zendo!" }
    puts "Sitting still…"  # This is not executed
end

meditate
Output:

Adjusting posture…
Ringing bell…
Sitting still…

Notice that no havoc was wreaked at the zendo but the line below the proc was executed.

Blocks and enumerators


The most common use of blocks is in enumerators.

In the examples below, note that all chunks of code enclosed by { } or do..end are blocks. Also, note that variable names enclosed by pipe characters (|) are arguments passed to the blocks.

a = ["zazen", "shamata", "tonglen", "vipassana"]
a.each { |meditation| puts meditation }

The example above can also be written in multi-line syntax using do and end instead of curly braces, like this:

a.each do |meditation|
  puts meditation
end

Both the Array and the Hash classes provide a method called each, which returns an Enumerator object. The each method accepts a block as a parameter. At each iteration of the enumerator, one item of the array is passed into the block as an argument.

Blocks are not closures


A closure is a method with the following characteristics:
  • It remembers the values of all variables that were available in the scope it was defined, even when called on another scope or when those variables are no longer available.
  • It is an object. Hence, it can be assigned to a variable, passed around, called from within different scopes, etc.
A block meets only the first of these two criteria, therefore it's not a closure. As for the second criteria, blocks are not first-class entities, they are not objects, cannot be assigned to variables. Unlike procs which have the Proc.new method, there is no Block.new method. Blocks also don't have a call method.

This point is controversial; many authors state that blocks are closures. However, in the book The Ruby Programming Language, written by Yukihiro Matsumoto (the creator of Ruby, AKA Matz), there is a section on closures (chapter 6, section 6) and it states "In Ruby, procs and lambdas are closures.". There is also this interview with Matz where he states "You can reconvert a closure back into a block, so a closure can be used anywhere a block can be used. Often, closures are used to store the status of a block into an instance variable, because once you convert a block into a closure, it is an object that can by referenced by a variable. And of course, closures can be used as they are used in other languages, such as passing around the object to customize the behavior of methods. If you want to pass some code to customize a method, you can of course just pass a block. But if you want to pass the same code to more than two methods -- this is a very rare case, but if you really want to do that -- you can convert the block into a closure, and pass that same closure object to multiple methods." If you disagree, please share your point of view by commenting. If you can prove beyond any doubt that a block is a closure, I will rewrite this post.

Blocks can be implicitly converted to procs with & (ampersand)


Blocks and procs are very similar. However, unlike a proc, a block is not an object and cannot be assigned to a variable.

A block can, however, be converted into a proc (which is a closure) when passed to a method with the argument name prefixed by & (ampersand). Example:

def meditate(&practice)
    p = practice  # Assign the proc to a variable. We would not be able to do that with a block.
    p.call if block_given?
end

meditate { "40 minutes of zazen." }
Output:

=> "40 minutes of zazen."

In the example above, prefixing the practice argument with an & implicitly converts the passed block to a proc by calling the to_proc method. That's why we can assign it to the p variable and execute it with the call method instead of yield. We would not be able to do that with a regular block. As the argument prefixed with & is also used as the method's block, we could also execute it with a yield statement, like this:

def meditate(&practice)
    yield
end

meditate { "40 minutes of zazen." }
Output:

=> "40 minutes of zazen."

Prefixing an explicit block argument with & will convert it to a proc. The opposite is also true: if we provide a proc as an argument prefixed with &, it will be converted into a block.

8 comments:

  1. Replies
    1. I'm glad you liked it. Thanks for your feedback.

      Delete
  2. I've recently found myself going down the rabbit hole of learning about closures-blocks-procs-lambdas, etc. and your post has helped clarify some things and point me in the direction of other things to keep learning (further down the rabbit hole). :-) I haven't found the "zen place" yet for understanding blocks as closures or not, but I'm getting close. I'm researching for my own blog post, so I'll come back and let you know when it's out. Thanks for posting this!

    ReplyDelete
    Replies
    1. Hello, Skylar. I agree this one of the many rabbit holes in Ruby and Rails. I'm glad you found this post to be helpful. Do let me know once your post is published. Thanks for your feedback :)

      Delete
    2. why don't you write any post recently, i'm really waiting for your posts. Please

      Delete
    3. Hello godness Phu,

      Thank you for your comment. Sorry about not being able to write new posts. I have been studying full time in the last few months, trying to improve my Ruby on Rails skills. Maybe I'll write some RoR posts in the months to come. Best Regards.

      Delete