One of the most unique and often misunderstood features of Ruby is blocks. Blocks are Ruby’s version of closures and can be used to make code more reusable and less verbose. But keywords such as yield
can be hard to grok at first and make this functionality a bit intimidating to work with. This article aims to go over block essentials and build up your knowledge piece by piece.
What is a block?
Blocks are anonymous functions whose return value will be applied to the method that invokes it. That’s quite a mouthful 😳 so let's work up our understanding.
Blocks consist of code between a set of curly braces or a do/end pair. The former is the single-line definition and the latter is the multiline definition.
method { |i| ... }
method do |i|
...
end
The single-line definition is primarily used for one-liners. For the sake of consistency I’ve used the do/end syntax in all of the examples
If you’ve worked with Ruby before, I almost guarantee you’ve seen a block. The each
and map
methods are two of the most commonly used iterators that invoke block usage.
["⭐️", "🌟"].each do |star|
puts star
end
# Output
⭐️
🌟
How do you create a block?
If we wanted to create a simple function that prints an input wrapped in "⭐️", we could write something like this…
def star_wrap(el)
puts "⭐️" + el + "⭐️"
end
star_wrap("💙")
# Output
⭐️💙⭐
If we wanted to rewrite this function using block notation, we could do it like so…
def star_wrap
puts "⭐️" + yield + "⭐️"
end
star_wrap do
"💜"
end
# Output
⭐️💜⭐
As shown above, the return value of the block contents are what is passed to the yield
keyword in the function.
We can also make the block more customizable by passing a parameter to the function we created.
def wrap_with(el)
puts el + yield + el
end
wrap_with("⭐️") do
"💚"
end
# Output
⭐️💚⭐️
If we want to refer to values from our function in the attached block, then we can pass arguments to yield
and reference it in the block parameters…
def wrap_with(el)
puts el * 5
puts yield(el * 2)
puts el * 5
end
wrap_with("⭐️") do |els|
els + "🖤" + els
end
# Output
⭐️⭐️⭐️⭐️⭐️
⭐️⭐️🖤⭐️⭐️
⭐️⭐️⭐️⭐️⭐️
But why use blocks over a regular function?
So far it doesn’t really seem like we’re doing anything that a typical method can’t do. Blocks come in handy when we want to apply shared logic to various contexts in an encapsulated way.
For example, let’s say we knew that we always wanted to print the output of a series of commands between a set of "⭐⭐⭐". We can use blocks to apply the wrapping logic to different contexts without having to make auxiliary functions.
def star_wrap
puts "⭐⭐⭐"
puts yield
puts "⭐⭐⭐"
end
star_wrap do
server = ServerInstance.new
data = server.get("orange/heart/endpoint")
data.to_s
end
star_wrap do
fetcher = DatabaseFetcher.new
data = fetcher.load("purple_heart_data")
data.exists? data : "no heart data"
end
# Output (hypothetical)
⭐⭐⭐
🧡
⭐⭐⭐
⭐⭐⭐
💜
⭐⭐⭐
As shown above, stars are always printed before and after the code executed in a block. Even though "🧡" was fetched quite differently than "💜"️, the star_wrap
method allows us to apply the star wrapping logic to both contexts in a contained manner.
Block error handling
Any method can accept a block, even if it is not referenced in the function. The block contents will simply do nothing.
def stars
puts "⭐⭐⭐"
end
stars do
puts "💙"
end
# Output
⭐⭐⭐
We were able to invoke all of the blocks in the examples above because we used the keyword yield
. So, what if we called yield
and did not supply a block? An error will be raised.
def star_wrap
puts "⭐️" + yield + "⭐️"
end
star_wrap
# Output
LocalJumpError: no block given (yield)
We can amend this issue by using the block_given?
expression to check for block usage.
def star_wrap
if block_given?
puts "⭐️" + yield + "⭐️"
else
puts "⭐️⭐️⭐️"
end
end
star_wrap
# Output
⭐️⭐️⭐️
Passing a block as a parameter
If we wanted to be more explicit in our invocation of a block and have a reference to it, we can pass it into a method as a parameter.
def star_wrap(&block)
puts "⭐️" + block.call + "⭐️"
end
star_wrap do
puts "💛"
end
# Output
⭐💛⭐
In this instance, the block is turned into a Proc object which we can invoke with .call
. Using blocks in this manner comes in handy when you want to pass blocks across functions. We specify the block parameter by passing it as the last argument and prepending it with &
.
Below, the methods star_wrap_a
and star_wrap_b
do the exact same thing…
def star_wrap_a(&block)
puts "⭐" + block.call("✨") + "⭐"
end
def star_wrap_b
puts "⭐" + yield("✨") + "⭐"
end
star_wrap_a do |el|
el + "💙" + el
end
star_wrap_b do |el|
el + "💚" + el
end
# Output
⭐✨💙✨⭐
⭐✨💚✨⭐
Blocks in the real world
In a default Rails app, the application.html.erb
view is loaded for every page whose controller inherits from ApplicationController
. If a child controller of ApplicationController
renders a view, its contents are yielded to application.html.erb
. With this functionality in place, boilerplate HTML that must be applied to all pages of the app can be done so easily.
<!DOCTYPE html>
<html>
<head>
<title>Block Investigation</title>
</head>
<body>
<%= yield %>
</body>
</html>
For example, in Gusto’s Partner Directory, the top blue bar resides in a base view file which yields to a different layout for each route.
And those are the essentials!
Blocks don’t have to contain complicated logic in order for us to make use of them. They are typically used to abstract away shared logic that can be applied to a multitude of contexts. If written thoughtfully, code can be made more DRY and readable through harnessing their functionality. To solidify any of the concepts mentioned above, I recommend trying out the code samples in an interactive console or by writing your own examples. Happy ⏹-ing.