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
Top comments (3)
Love the article and the examples. The
UserImpersonator
is such a perfect example of using the language to your advantage.Thanks! I'm really glad you found it useful :)
I'm currently trying to understand(more) how blocks work in order to identify when they be useful for me.
The
UserImpersonator
example is a step forward in this journey I'm making.Thanks for sharing!