Want to see the full-length video right now for free?
Sign In with GitHub for Free AccessBlocks are a core concept in Ruby, and while they make frequent appearances in Ruby code, developers may not be aware of exactly how they work or all the things they can do. In this video, Boston Development Director Josh Clayton walks through this powerful Ruby language feature, showing how blocks work and how you can effectively use them in your own code.
The Climate Control gem allows a test to be run with its own
set of temporary environment variables (usually accessed through ENV
).
It uses blocks to establish a contained scope for the test and its modified
environment without polluting the general environment for other tests.
RSpec.describe Environment do
it "modifies the ENV only when the block is run" do
updated_env = nil
Environment.set FOO: "bar" do
updated_env = ENV["FOO"]
end
expect(updated_env).to eq "bar"
expect(ENV["FOO"]).to be_nil
end
end
Blocks can be delineated with the familiar do
and end
or with curly braces.
(Remember that not all curly braces represent blocks! Sometimes they signify a
hash.) The block allows the establishment of a lexical scope for an anonymous
method.
# a block using do-end
within ".body" do
has_css(".nav")
end
# a block using {}
2.times { puts "hello" }
&
preceding an argument (such as &block
) designates a block argument in a
method's argument list. In this method, &block
means the argument will be a
block:
def run(&block)
begin
cache_old_values
assign_env
block.call
ensure
reset_env
end
end
Within a method, a block argument can be executed by sending it #call
.
Keep in mind that the block can be called multiple times!
yield
and explicit context)When you define a block, you can specify "block variables" (declared between
pipe characters, such as |memo|
) that are only accessible within the block.
result = Calculator.run(start: 5) do |c|
c.add 5
end
Within a method #yield
provides a separate means of invoking a block (in
addition to the previously mentioned Proc#call
), with the ability to pass
arguments to a block (which then become block arguments). You can see #yield
in Rails layouts, when the layout yields to the specified view.
def self.run(start:)
yield Operations.new(start)
end
The block's execution will return the value of the last evaluation of the block. This behavior allows the building of chainable methods.
result = Calculator.run(start: 5) do |c|
c.add 5
c.multiply_by 2
c.subtract 15
end
expect(result).to eq 5
Object#instance_exec
We discussed #call
and #yield
as ways to invoke a block. There is a separate
third way to invoke a block. Object#instance_exec
allows you to run a block in
the context of the calling object, meaning that references in the block
(including self
) refer to the calling object. Here's the example from Ruby's
documentation:
class KlassWithSecret
def initialize
@secret = 99
end
end
k = KlassWithSecret.new
k.instance_exec(5) {|x| @secret+x } #=> 104
Within the block, @secret
references the instance variable in the object that
received #instance_exec
with the block.
FactoryGirl makes use of #instance_exec
to set attributes in a factory. In
this simplified factory example, #instance_exec
is used with #method_missing
to dynamically build a hash key-value pairs out of method-argument pairs. The
methods declared in the block will be executed within the runner's context,
where they will not be defined. The runner will then delegate those method calls
(with their arguments) to #method_missing
, where the implementation will build
up the hash's keys and values. (Note that #method_missing
is not part of
Ruby's block functionality, but it is often used with it.)
def method_missing(name, *args, &block)
@result[name.to_sym] = args.first
end
The #block_given?
method can be called within a method to check
if a block argument was supplied. Here Josh uses #block_given?
to allow the
HashDSL
class to build a hash with an arbitrary level of nested values using
recursion:
def method_missing(name, *args, &block)
@result[name.to_sym] = if block_given?
Runner.new.instance_exec(&block).__result__
else
args.first
end
self
end