Blocks are a fundamental concept in Ruby. Many common Ruby methods use blocks. Blocks are also an integral part of many domain-specific languages (DSLs) in libraries like RSpec, Factory Bot, and Rails itself.
In this post we’ll discuss what a block is. Then we’ll take a look at four different native Ruby methods that take blocks (times
, each
, map
and tap
) in order to better understand what use cases blocks are good for.
Lastly, we’ll see how to define our own custom method that takes a block.
What a block is
Virtually all languages have a way for functions to take arguments. You pass data into a function and then the function does something with that data.
A block takes that idea to a new level. A block is a way of passing behavior rather than data to a method. The examples that follow will illustrate exactly what is meant by this.
Native Ruby methods that take blocks
Here are four native Ruby methods that take blocks. For each one I’ll give a description of the method, show an example of the method being used, and then show the output that that example would generate.
Remember that blocks are a way to pass behavior rather than data into methods. In each description, I’ll use the phrase “Behavior X” to describe the behavior that might be passed to the method.
Method: times
Description: “However many times I specify, repeat Behavior X.”
Example: three times, print the text “hello”. (Behavior X is printing “hello”.)
3.times do
puts "hello"
end
Output:
hello
hello
hello
Method: each
“Take this array. For each element in the array, execute Behavior X.”
Example: iterate over an array containing three elements and print each element. (Behavior X is printing the element.)
[1, 2, 3].each do |n|
puts n
end
Output:
1
2
3
Method: map
“Take this array. For each element in the array, execute Behavior X, append the return value of X to a new array, and then after all the iterations are complete, return the newly-created array.”
Example: iterate over an array and square each element. (Behavior X is squaring the element.)
squares = [1, 2, 3].map do |n|
n * n
end
puts squares.join(",")
Output:
1,4,9
Method: tap
“See this value? Perform Behavior X and then return that value.”
Example: initialize a file, write some content to it, then return the original file. (Behavior X is writing to the file.)
require "tempfile"
file = Tempfile.new.tap do |f|
f.write("hello world")
f.rewind
end
puts file.read
Output:
hello world
Now let’s look at how we can write our own method that can take a block.
Custom methods that take blocks
An HTML generator
Here’s a method which we can give an HTML tag as well as a piece of behavior. The method will execute our behavior. Before and after the behavior will be the opening and closing HTML tags.
inside_tag("p") do
puts "Hello"
puts "How are you?"
end
The output of this code looks like this.
<p>
Hello
How are you?
</p>
In this example, the “Behavior X” that we’re passing to our method is printing the text “Hello” and then “How are you?”.
The method definition
Here’s what the definition of such a method might look like.
def inside_tag(tag, &block)
puts "<#{tag}>" # output the opening tag
block.call # call the block that we were passed
puts "</#{tag}>" # output the closing tag
end
Adding an argument to the block
Blocks can get more interesting when add arguments.
In the below example, the inside_tag
block now passes an instance of Tag
back to the block, allowing the behavior in the block to call tag.content
rather than just puts
. This allows our content to be indented.
class Tag
def content(value)
puts " #{value}"
end
end
def inside_tag(tag, &block)
puts "<#{tag}>"
block.call(Tag.new)
puts "</#{tag}>"
end
inside_tag("p") do |tag|
tag.content "Hello"
tag.content "How are you?"
end
The above code gives the following output.
<p>
Hello
How are you?
</p>
Passing an object back to a block is a common DSL technique used in libraries like RSpec, Factory Bot, and Rails itself.
The technical details of blocks
There are a lot of technical details to learn about blocks. There are some interesting questions you could ask about blocks, including the following:
- What are the different ways to call blocks and why would you use each?
- What’s the difference between a proc, a block, and a lambda?
- What’s a Proc object and how does it relate to a block?
- What’s a closure?
- What does the & at the beginning of &block mean?
- When I do e.g.
[1, 2, 3].map(&:to_s)
, how does that work?
These are all good questions worth knowing the answer to, and you can click the links above to find out. But understanding these details is not necessary in order to understand the high-level gist of blocks.
Takeaway
A block is a way of passing behavior rather than data to a method. Not only do native Ruby methods make liberal use of blocks, but so do many popular Ruby libraries. Custom methods that take blocks can also sometimes be a good way to add expressiveness to your own applications.
Thanks for such an elaborate explanation of ruby blocks!
Simple and clear.
Thank you for this content
Thank you for such wonderful article. I have learned something new today.
Pingback: Railsの技: カスタムヘルパーとStimulusで軽量コンポーネントを構築(翻訳)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社
Thanks for this creative explanation! I recently finished the Ruby Blocks course from Pragmatic Studio and have to say that, while I recognized the ‘execute around’ pattern that they teach, this specific example was missing.
Probably I couldn’t understand the above example without that course, but have to say that the ‘come back’ to `respond_to` method has been really helpful to connect the theory to code we see every day 👍
You describe what block does and it’s purpose, but you never really define what a block actually is. It’s an anonymous method that can be passed as a parameter in other methods.