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.time
s 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