Arjun Rajkumar
Posted on October 28, 2022
Recent articles from Progress Updates:
- The only daily stand up meeting template you need
- The only two questions you need to ask in your daily standup scrum
- The only daily standup meeting agenda you will ever need
- How to Create a Weekly Report With Templates and Examples
- How to automate your end of week status reports
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)}
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
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
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
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
- 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
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.
- 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
- 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
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")
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
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
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
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
And in Active Record's life cycle hooks.
class User < ActiveRecord::Base
before_create do |user|
puts "about to create #{user.name}"
end
end
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.
Posted on October 28, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.