How to take advantage of Ruby blocks

mickeytgl

Miguel

Posted on May 29, 2019

How to take advantage of Ruby blocks

Blocks in ruby are a very powerful aspect of the language. They are often less used in when compared to other features of the language and it is something not everyone is comfortable using them, and much less writing code that consumes blocks. So let's start with a small introduction:

What are blocks?
Essentially, a method with no name. It is a piece of code, delimited either by { } or by do... end. That we are able to call in a certain context For example:

# The following syntaxes are equivalent. They will both execute the block twice.

2.times do
  puts "foo"
end

2.times { puts "foo" }

A more complicated example: Let's assume that for a user masquerading gem, you want a method in which you can pass in a user, grant them all permissions, run some code with those permissions in place, and then reset the old permissions once the code has finished running. This is where blocks shine. A very simple test suite for this would be something like:

RSpec.describe UserImpersonator do
  it "grants permissions while inside the block" do

    UserImpersonator.grant_all_permissions valid_user do
      expect(valid_user.roles[:admin]).to be_true
    end

    expect(valid_user.roles[:admin]).to be_false
  end
end

We first need a UserImpersonator class and a grant_all_permissions method that accepts the user, and the block of code you want to run while you have all permissions in place.

class UserImpersonator
  def self.grant_all_permissions(user, &block)
    new(user).run(&block)
  end

  def initialize(user)
    @user = user
  end

  def run(&block)
    begin
      cache_old_permissions
      assign_all_permissions
      block.call
    ensure
      reset_permissions
    end
  end
end

Let's break up the code above:

The &block on the self.grant_all_permissions method definition lets the method know that it can expect to be passed in a block. This will instantiate a new UserImpersonator object and call the run method on it, forwarding the &block argument.

The run method's first two lines after the begin keyword are the set up, we want to cache the old permissions so that we can reset them later and assign the new ones. After this, we want to actually run the block that was passed in, and after that, we want to reset the permissions back, regardless of whether or not an error was raised somewhere in between, hence the ensure keyword.

There you have it, we've just created a method that accepts a block and runs it wherever we want, this is certainly useful, but we can go deeper

Let's try now passing a specific context into our block, or block variables, if you have used rails, even for a bit, then you have probably seen this when you iterate through your database records, for example:

User.all.each do |user|
  user.update admin: false
end

The block variable is what is between the pipes, user in this case.

New example: Let's imagine we're running a restaurant, and we would like to calculate the cost of an order. Since an order can be made out of different items, we want to be able to add as much or as little items within the context of that order. Again, starting with the tests, we could have an expectation like:

RSpec.describe Order do
  it "allows to make operations" do
    result = Order.cost do |c|
      c.add_taco
      c.add_taco
      c.add_guacamole
      c.add_beer
    end

    expect(result).to eq 44
  end
end

To make the test above pass, one possible implementation that would make the test above to pass would be:

class Order
  def self.cost
    yield Actions.new
  end

  class Actions
    def initialize
      @cost = 0
    end

    def add_taco
      @cost += 10
    end

    def add_guacamole
      @cost += 6
    end

    def add_beer
      @cost += 18
    end
  end
end

Things to note:

1) The block variable, or c in the case of the test is an instance of the Actions class, which means it has access to the add_taco, add_guacamole and add_beer methods

2) You don't need to specify &block as an parameter in the cost method since you are using yield inside of the method

3) yield makes a block an OPTIONAL parameter, which means that you COULD call cost and pass no block at all. This however, will fail and complain with a

LocalJumpError (no block given (yield))

To solve this, a handy little method called block_given? allows you to verify if a block was passed in. So you could refactor the cost method to handle that:

def self.cost
  yield Actions.new if block_given?
end

I really hope that this deep dive into the more fun and out there features of ruby was useful. Personally, I find it enlightening to find out how the inner workings of the tools I use very often within RSpec or factory_bot fit together step by step

💖 💪 🙅 🚩
mickeytgl
Miguel

Posted on May 29, 2019

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

Sign up to receive the latest update from our blog.

Related