Ruby Blocks Simplified

1_pINZvZUsJi2ew1_WOOoAgA

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.

1_gO9ujfjzcOD3DDkigbFv-g

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.