DEV Community

Maryna Nogtieva
Maryna Nogtieva

Posted on

Ruby Blocks

Ruby Block - what is that, and what does it do?

It has long been said that "Everying is an object in Ruby". Well, it is almost true.

Despite such classes as Class, Module, as well as many others eventually being objects, there's an exception to the rule.
As you probably guessed, block is not an object. We cannot assign it to a variable or call a method class on it.

It is a chunk of code that we want to execute dynamically by putting it between do...end or between curly braces {...}. The benefit of blocks is that we don't need to store some statements into a method. Blocks serve as part of a method signature (method signature includes method name, list of arguments and a block) and must start on the same line after method arguments.

We often use blocks when we read or write into files, or when we want to perform certain operations while iterating via a collection, such as an Array or a Hash.

As an example let's take a look into times method of the Integer class.

# Block written in one line
5.times { puts "Some code within a block" }

# Block written in multiple lines
5.times do
  puts "Some code within a block"
end
# Output in both cases
Some code within a block
Some code within a block
Some code within a block
Some code within a block
Some code within a block
=> 5

As per the implementation of the times method, an output Some code within a block will be printed out five times and integer 5 will be returned after that.

A block has to start on the same line as the method name and its parameters if any.

# This is wrong
5.times
  { puts "Some code within a block" }

Difference between do..end and curly braces {}.

  • According to the Ruby Style Guide it is recommended to use do...end for multi-line blocks and {...} for single-line blocks.
  • Curly braces {...} syntax has higher precedence than do...end syntax. We can observe when a block is passed to a method without using parentheses ():
# Using {...}
puts [1,2,3].select{ |number| number.even? }
2
=> nil # method "puts" returns nil

# Using do...end
puts [1, 2, 3].select do |number| number.even? end
#<Enumerator:0x00007fefbebca498>
=> nil # method "puts" returns nil

In the first case, we see 2 is printed back because the method puts accepts everything till the end of the block as one parameter.
In the second case, enumerator is returned instead because the method puts does not see do..end as a part of a parameter.

Block Parameters

A block can have parameters that are stored between vertical lines called pipes ||.

2.times do |number|
  puts number
end
# Output printed
0
1
=> 2 # returned value of the "times" method

Block Variable Scope

Block variables (parameters between the pipes ||) can behave differently from variables that are coming from the method scope. Let's review some examples:

  • A variable is declared in the method and the same variable is used inside a block. We can see that block has access to the x:
def block_variable_example
  x = 1
  3.times do
    puts "x inside the block: #{x}"
  end

  puts "x outside the block: #{x}"
end
# Call method
block_variable_example

# Output
x inside the block: 1
x inside the block: 1
x inside the block: 1
x outside the block: 1

=> nil
  • A variable is declared in the method and the same variable is being re-assigned inside the block. Same as in the first example, variable inside a block is the same as the one outside:
def block_variable_example
  x = 1
  3.times do
    x = 2
    puts "x inside the block: #{x}"
  end

  puts "x outside the block: #{x}"
end
# Call method
block_variable_example

# Output
x inside the block: 2
x inside the block: 2
x inside the block: 2
x outside the block: 2

=> nil
  • A variable x is declared in the method and the block parameter has the same name x. It appears that variable inside the block is different from the one that was declared in the method:
def block_variable_example
  x = 1
  3.times do |x|
    puts "x inside the block: #{x}"
  end

  puts "x outside the block: #{x}"
end

# Call method
block_variable_example
# Output
x inside the block: 0
x inside the block: 1
x inside the block: 2
x outside the block: 1

=> nil

What is yield keyword?

If you want to execute a block inside a method yield is your friend. In many countries, there's a traffic sign yield which means you need to give right of way to another car.

  • Similar logic applies here. During a method execution when Ruby parser sees yield it gives the right of way to the code within a block and executes it. After that, it comes back to the method scope and executes the remaining code.
  • In case there are situations when a block is going to be optional when calling your method it is better to use the Kernel#block_given? method so that no exception is thrown. Here are some examples of using yield keyword:
def my_yield_method
  puts "Code before yield"

  if block_given?
    yield
  else
    puts "No block was given"
  end

  puts "Code after yield"
end

# =================== Call method with a block
my_yield_method { puts "Code inside a block" }
# Output
Code before yield
Code inside a block
Code after yield

=>nil
# =================== Call method without a block
my_yield_method
# Output
Code before yield
No block was given
Code after yield

=> nil
  • yield keyword can have arguments that are going to be passed to a block
def my_yield_method_with_params
  str = "Awesome parameter"

  puts "Code before yield"
  yield str if block_given?
  puts "Code after yield"
end

# =================== Call method with a block
my_yield_method_with_params do |param|
  puts "This is what we got: #{param}"
end
# Output:
Code before yield
This is what we got: Awesome parameter
Code after yield

=> nil

I hope you found this information helpful 🙂

Top comments (3)

Collapse
 
cescquintero profile image
Francisco Quintero 🇨🇴 • Edited

Hi Maryna, nice explanation here.

I've been coding in Ruby for many years and until last year found myself that I don't use blocks in the code I write in a daily basis xD

I don't feel bad for that but I do wonder what's the real use case of blocks when we create our own functions? I mean, I know how to use the ones in the Ruby API but I don't see cases where a function I wrote would benefit from a block.

Example, I know I can do:

result = ary.map { |item| # some calculation }

but when I write something like this

def my_function(args)
end

I've never added a block to the functions I write xD

I'd like to extent the question to you to know your opinion :)

Collapse
 
marynanogtieva profile image
Maryna Nogtieva • Edited

Hi Francisco,

Thank you for your feedback and an interesting question 🙂
I also do not write custom methods that yield blocks too often. One example that currently comes to my mind is when I was working with Net::SFTP to perform various operations for sftp files: download, upload, rename, get all files.

We also had different ways to connect to SFTP server - via user/password or via ssh/no password.

It could result into something like this

Net::SFTP.start(server, username, { password: password, port: port}) do |sftp|
  sftp.upload!("/local/file.tgz", "/remote/file.tgz")
end

Net::SFTP.start(server, username, { keys: [ssh_key_path] }) do |sftp|
  sftp.upload!("/local/file.tgz", "/remote/file.tgz")
end

One way to make it less redundant would be to create a method that accepts a block (with syntactic sugar parameter &block):

# Some pseudocode will be here
def flexible_sftp(&block)
   if use_ssh?
      return Net::SFTP.start(server, username, { keys: [ssh_key_path] }, &block)
   else
     return Net::SFTP.start(server, username, { password: password, port: port }, &block)
   end
end

 flexible_sftp do |sftp|
    sftp.upload!("/local/file.tgz", "/remote/file.tgz")
 end

Of course, it might be possible to avoid using a custom method here and instead pass some sort of argument hash or object into the method. But the code above is one way to solve the problem.

Another time I used yield is when I used to run performance metrics with RubyProf

RubyProf.measure_mode = RubyProf::WALL_TIME

INITIAL_DATA_FILE = 'fixtures/small.json'
OUTPUT_DIR = 'tmp/data'


def flat_profile
  run_profiler do |result|
    printer = RubyProf::FlatPrinterWithLineNumbers.new(result)
    printer.print(File.open("#{OUTPUT_DIR}/ruby_prof_flat_demo.txt", 'w+'))   
  end
end

def graph_profile
  run_profiler do |result|
    printer = RubyProf::GraphHtmlPrinter.new(result)
    printer.print(File.open("#{OUTPUT_DIR}/ruby_prof_graph_demo.html", "w+"))
  end
end

def run_profiler
  RubyProf.measure_mode = RubyProf::WALL_TIME
  result = RubyProf.profile { JsonImporter.new.import_json_to_db(file_path: INITIAL_DATA_FILE) }
  yield result
end
Collapse
 
cescquintero profile image
Francisco Quintero 🇨🇴 • Edited

Awesome. Thanks for your answer.

I've been asking this question to every one I can (last's year RubyConfColombia I even asked Aaron Patterson 😅) and the answers is almost the same: "not [too] often".

Your examples are really good and this was precisely why I asked. Getting to see blocks being used outside the Ruby API is a way to experience their benefit or possibilities.

Thanks again!