Understanding Blocks in Ruby

Blocks are an essential functional programming feature in Ruby. A block is typically a chunk of code only bound to a method from which it can be invoked. It can’t be stored in a variable and isn’t an object. Ruby itself leverages blocks for its built-in methods, giving us the convenience of using them. We can also use the block feature to add flexibility to our defined methods.

What a block looks like

A block is usually enclosed in a do ... end statement for a multi-line block or in braces { ... } for a single-line block, preceded by a method.

Regarding the best practice, it is recommended to use the do ... end syntax for the multi-line block.

ALPHABET = ('A'..'F').to_a
random_alphabets = []
8.times do
 random_alphabets << ALPHABET.sample
end

puts random_alphabets.join(', ')

This creates an array of 8 random alphabets. The insertion operation occurs inside the block following 8.times method, repeating 8 times.

The example demonstrates a do ... end block, even though it contains only one line of code. Let’s convert it into a single-line block.

ALPHABET = ('A'..'F').to_a
random_alphabets = []
8.times { random_alphabets << ALPHABET.sample }

puts random_alphabets.join(', ')

Notice that after 8.times is followed by a code statement wrapped within the brackets.

Overview of Ruby methods that take blocks

Many Ruby’s built-in methods accept blocks, yet we will look at the Array’s most often used methods, such as each, map, find, and select, for instance.

Each

Travel through the array and pass the array element to the given block that handles the operation repeatedly.

['orange', 'apple', 'banana', 'mango'].each do |fruit|
 puts "This fruit is '#{fruit.capitalize}'"
end

Output

This fruit is 'Orange'
This fruit is 'Apple'
This fruit is 'Banana'
This fruit is 'Mango'

Map

Commonly used to create a new array based on the original array through the operation in the given block.

country_names_with_codes = [
 ['Argentina', 'AR'],
 ['Belgium', 'BE'],
 ['Cambodia', 'KH'],
 ['Denmark', 'DK'],
]

country_names_with_codes.map do |elements| 
 "#{elements[0]} (#{elements[1]})"
end

Output

=> ["Argentina (AR)", "Belgium (BE)", "Cambodia (KH)", "Denmark (DK)"]

The result is the new array with modified values due to the block.

Find

Search through an entire array and return the first array element that meets the condition in the given block. The process of iteration terminates as soon as it meets the first match.

countries = [
'Argentina', 
'Belgium', 
'Cambodia', 
'Cameroon', 
'Canada', 
'China', 
'Denmark'
]
countries.find { |country| country == 'Cambodia' } 

# => "Cambodia"

countries.find { |country| country.start_with?('C') } 

# => "Cambodia"

Select

Return a new array with elements filtered from the original array for which the condition matches, due to the given block.

countries = [
'Argentina', 
'Belgium', 
'Cambodia', 
'Cameroon', 
'Canada', 
'China', 
'Denmark'
]
countries.select { |country| country.start_with?('C') } 

# => ["Cambodia", "Cameroon", "Canada", "China"]

Define your method that accepts a block

When you write your method that takes a block, you need to handle invoking it on your own by calling yield.

Yield commands the execution to the block and can also pass arguments to the block in the same way as the methods.

Let’s dive into some variations of usage:

Yield

def welcome(name = "World")
  puts "Hi #{name},"
  yield
  puts "Happy coding in Ruby!"
end

welcome("Tom") { puts "Welcome to Ruby!" }

# Output
# Hi Tom,
# Welcome to Ruby!
# Happy coding in Ruby!

welcome("Jerry") do 
  puts "Let's get started with Ruby!"
  puts "Hello World!"
  puts "Yeah...!"
end

# Output
# Hi Jerry,
# Let's get started with Ruby!
# Hello World!
# Yeah...!
# Happy coding in Ruby!

Yield is called without arguments from within welcome method, and the statements of the block are executed accordingly.

Yield with arguments

def multiply(a, b)
  yield(a * b)
end

multiply(10, 5) { |result| puts "The result is #{result}" } 

# Output
# The result is 50

multiply(1.25, 5) do |result| 
  puts "The result is #{result}"
  puts "When round down:"
  puts "The result is #{result.floor}"
  puts "When round up:"
  puts "The result is #{result.ceil}"
end

# Output
# The result is 6.25
# When round down:
# The result is 6
# When round up:
# The result is 7

Here you can see that yield passes a * b argument for the block, and the block can use it for its purpose. Moreover, you can pass even more complex arguments with yield, like with the methods.

Yield If Block Is Given

class BlogPost
  attr_accessor :status

  STATUSES = %w(draft pending reviewed published)

  def initialize(status = "draft")
    @status = status
  end

  def publish
    raise "Cannot be published" unless status == "reviewed"

    @status = "published"

    yield if block_given?
  end
end

post = BlogPost.new
post.status = "reviewed"
post.publish do
  puts "Blog post has been successfully published."
  puts "Send an email to your audience."
end

# Output
# Blog post has been successfully published.
# Send an email to your audience.

Generally, when your method accepts a block and you call it without passing a block, it will raise an error no block given (yield). But you can prevent it by checking if the block is given before yielding.

In the above example, the publish method calls yield if the block is given. After the post has been successfully published, we can execute the behavior we aim to do with a given block. Otherwise, it has just updated the status of the blog post from reviewed to published.

Invoking an explicit block

def welcome(greeting, name, &block)
  puts "#{greeting}, #{name}"
  block.call(name)
  puts "Best regards"
end

welcome('Hi', 'Bob') do |name|
  puts "Welcome to my website!"
  puts "#{name}, thank for creating your account with us."
end

# Output
# Hi, Bob
# Welcome to my website!
# Bob, thank for creating your account with us.
# Best regards

Sometimes, we specify an argument of a block named &block for the method. In this case, we invoke the block calling block.call(name). It is the same as calling yield with or without passing arguments to the block.

The specified block argument &block, in this case, is a proc object and must be invoked by calling proc.call. Also, it can be passed to another method or stored in a variable. This is a significant difference from a common block.

Let’s look at another example:

class Greeter
  def initialize(greeting, &block)
    @greeting = greeting
    @message = block
  end

  def to(name)
    @message.call(@greeting, name)
  end
end

hi = Greeter.new("Hi") { |greeting, name| puts "#{greeting}, #{name}" }
hi.to("Bob")

# Output
# Hi, Bob

hello = Greeter.new("Hello") { |greeting, name| puts "#{greeting}, #{name}" }
hello.to("John")

# Output
# Hello, John

The block that is responsible for crafting the greeting message is passed to the initialize method and assigned to a variable named @message. The block (@message) is invoked, printing the greeting message, until hi.to("Bob") is called.

A block can return a value

Interestingly, a block can return a value. The value will be retrieved from within the method that yields to the block.

def add(x, y)
 if yield(x, y)
  puts "The result is #{x + y}" 
 else
  puts "The operation cannot be processed!"
 end
end

add(10, 20) { |x, y| x.even? && y.even? }
# Output
# The result is 30

add(10, 15) { |x, y| x.even? && y.even? }
# Output
# The operation cannot be processed!

add(10, 20) { |x, y| x.positive? && y.positive? }
# Output
# The result is 30

add(10, -20) { |x, y| x.positive? && y.positive? }
# Output
# The operation cannot be processed!

Conclusion

A Ruby block is a group of code statements that is ONLY passed to a method, meaning it cannot be defined as a standalone group of statements or stored in a local variable. Despite everything being an object in Ruby, a block isn’t one; on the contrary.

Many of Ruby’s built-in methods accept blocks, allowing us to customize their behaviors conveniently. We can also define our methods by taking blocks to make them more functional and easier to use.

Happy Digging For Ruby!!! 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *