Understand Ruby Blocks by looking at examples from popular gems

arjunrajkumar

Arjun Rajkumar

Posted on October 28, 2022

Understand Ruby Blocks by looking at examples from popular gems

Recent articles from Progress Updates:


Ruby Blocks is something all Ruby developers use everyday. For example, while using the each, times, or upto methods or the countless other Enumerable module methods, we normally pass a block along with the method. For example:

1.upto(5) {|i| puts(i)}
Enter fullscreen mode Exit fullscreen mode

The code included in the brackets {} is passed to the upto method. This isn't passed as an argument, but as a block, and it is yielded by the upto method.

There are many different ways to use blocks, and here are some common patterns for using blocks:

1. Execute around

This pattern is mainly used when you have some common code you need to run before or after something else. For example, suppose you have a benchmark method like below:

def with_benchmark
  start_time = Time.now
  puts "Starting benchmark at #{start_time}"

  yeild

  stop_time = Time.now
  puts "The method took #{stop_time - start_time} to complete"
end
Enter fullscreen mode Exit fullscreen mode

Now you can find out how long any method takes to complete by doing something like this:

with_benchmark do
  # Call the signup method  
end

with_benchmark do
  # Call any other method to find out how long it takes to run 
end
Enter fullscreen mode Exit fullscreen mode

You could even improve the benchmark method to take an argument, and compare that with the result of the yield. For example:

def with_benchmark(expected_time)
  start_time = Time.now
  puts "Starting benchmark at #{start_time}"

  result = yeild

  stop_time = Time.now
  puts "The method took #{stop_time - start_time} to complete"

  if result > expected_time
    puts "Failed! Took too long to complete"
  else
    puts "Passed! That was super fast!"
  end
end

with_benchmark(5) do
  # Do anything you want
  sleep 10
end

with_benchmark(5) do
  # Call some method
  sleep 2
end
Enter fullscreen mode Exit fullscreen mode

Examples of 'Execute around' from gems:

The Ruby standard library has a benchmark module that includes methods similar to what we have written above:

def realtime
  r0 = Time.now
  yield
  Time.now - r0
end
Enter fullscreen mode Exit fullscreen mode
  1. It is not necessary that we have to run something before and after the yield. Sometimes, we can run only the before part, or only the after part. For example, you may need to change the state of something before yielding. The Shopify App gem maintained by Shopify, in the activate_shopify_session method, activates a Shopify session before yielding the block.
module ShopifyApp
  module LoginProtection
    ...
    def activate_shopify_session
      ...
      begin
        ShopifyAPI::Context.activate_session(current_shopify_session)
        yield
      ensure
        ShopifyAPI::Context.deactivate_session
      end
    end
Enter fullscreen mode Exit fullscreen mode

Like in the example above, it is also always recommended to use an ensure clause to change the state back incase the yielded block raises an error. As the code in the ensure clause is always executed, whether or not an exception is raised, it's guaranteed to return the state to how things were before you called yield.

2. Initializing an object with default values

Another common pattern of using blocks is to initialize an object with default values.

  1. In Rails, you can create a new object in many different ways:
# By passing the attribues as a hash
Shop.new(name: "Starbucks")

# By setting the attributes after initializing or creating the object
shop = Shop.new
shop.name = "Starbucks"

# Or by using blocks!
shop = Shop.new do |s|
  s.name = "Starbucks"
end
Enter fullscreen mode Exit fullscreen mode
  1. While creating a new Ruby gem
Gem::Specification.new do |s|
  s.name        = 'example'
  s.version     = '0.1.0'
  s.licenses    = ['MIT']
  s.summary     = "This is an example!"
  s.description = "Much longer explanation of the example!"
  s.authors     = ["Ruby Coder"]
  s.email       = 'rubycoder@example.com'
  s.files       = ["lib/example.rb"]
  s.homepage    = 'https://rubygems.org/gems/example'
  s.metadata    = { "source_code_uri" => "https://github.com/example/example" }
end
Enter fullscreen mode Exit fullscreen mode

This pattern is useful if you want to make the code more readable, and explicitly state what are the attributes required while setting up a new object.

To initialize an object with a block, you can do it by yielding the object on initialization. For example, if we had a simple Shop class, we could do it this way:

class Shop
  attr_accessor :name

  def initialize
    @name = "Starbucks"
    yield(self) if block_given?
  end

  def to_s
    "Name: #{@name}"
  end
end

shop = Shop.new
puts shop
# (would output "Name: Starbucks")

shop = Shop.new do |s|
 s.name = "Coffee Day"
end
puts shop
# (would output "Name: Coffee Day")
Enter fullscreen mode Exit fullscreen mode

3. Saving blocks to execute later

This is something I came across while reading Eloquent Ruby. This pattern is useful when you want to use blocks to be called only when required - i.e. you can hang on to a block until you need to use it.

Until now we have called blocks implicitly using the yield to fire off the block. However, blocks can also be captured as a parameter, and can then be run by calling the call method on it. Calling blocks explicitly has it's own advantages and is clearer to someone reading the code.

This is an example from the Pay gem.

module Pay
  module Webhooks
    class Delegator
      ...

      # Configure DSL
      def configure(&block)
        raise ArgumentError, "must provide a block" unless block
        block.arity.zero? ? instance_eval(&block) : yield(self)
      end
      ...
    end
  end
 end
Enter fullscreen mode Exit fullscreen mode

Passing blocks explicitly can also be really useful if you have an expensive method, and you only want to run it when really needed. Eloquent Ruby, shows a useful application for this. Suppose you have a Document class, and most times you only need to fetch the title and the author. Occasionally, you may need to read the actual content of the document from different sources that are chosen by the user.

class Document
  attr_reader :title, :author

  def initialize(title, author, &block)
    @title = title
    @author - author
    @initializer_block = block
  end

  def content
    if @initializer_block
      @content = @initializer_block.call
      @initializer_block = nil
    end
    @content
  end
end
Enter fullscreen mode Exit fullscreen mode

Implementing the document class this way means we can can get the contents from a file, or via HTTP, or via FTP.

file_doc = Document.new('file', 'Eloquent Ruby') do
  File.read('some_text.txt')
end

google_doc = Document.new('http', 'Eloquent Ruby') do
  Net::HTTP.get_response('www.google.com', '/index.html').body
end
Enter fullscreen mode Exit fullscreen mode

The above examples does look similar to the initializing examples below - but the main difference is that the Document class waits until the content method to fire off the block. If the content method is never called, the block will also never get called.

We can also see more examples of this pattern where the block is saved for later in before_action in Rails.

class ApplicationController < ActionController::Base
  before_action do |controller|
    # do something before each action
  end
end
Enter fullscreen mode Exit fullscreen mode

And in Active Record's life cycle hooks.

class User < ActiveRecord::Base
  before_create do |user|
    puts "about to create #{user.name}"
  end
end
Enter fullscreen mode Exit fullscreen mode

Conclusion
Using blocks with yield may take some time to get used to, but it gives us a different style of programming, that can improve the design of our code.

If you are looking for a senior Rails developer to join your team, do send me an email to arjun.rajkumar@hey.com. I took 6 months off as I became a new dad, but am currently open to new Ruby/Rails related work.

This post originally appeared on Weightless.

💖 💪 🙅 🚩
arjunrajkumar
Arjun Rajkumar

Posted on October 28, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related