Blocks, procs and lambdas
19 March 2022 | Updated on
When interviewing for my current position, the main question at the interview I stumbled over was “explain the difference between blocks and procs”. Here, I’m going to explore that, and throw in the closely related subject of lambdas for good measure.
Blocks
Blocks are a fundamentally important concept in Ruby, though it’s perfectly
possible to use them all the time without realising you’re doing so. That
section between do
and end
? That’s a block. Anything between curly braces
(excepting hash declarations)? That’s a block. The section at the beginning
between pipes? Those are arguments given to the block.
With that context, it’s clear that when we say “block”, we mean block of code. What differentiates these blocks of code from any other arbitrary chunk of code? They can be passed to a method (or, to put it another way, sent as a message to an object).
Why is this helpful? Well, one fairly obvious way is it gives us the ability to
use the code in a loop (e.g. with each
or using map
). But there are a wide
variety of ways blocks are helpful; by giving us the ability to pass a chunk of
code around it gives us the ability to manipulate it and use it in different
contexts.
Calling blocks
So you write a method that you want to take a block. How?
def run_a_block
yield 'hello'
yield 'world'
end
Here we see the keyword you need is yield
. Also note there’s no need to
specify that the method can take a block as an argument; any method can be
passed a block (of course many will simply ignore it). A block passed in this
manner is known as an implicit block. With that yield
we can also pass
arguments to the block, as you can see.
But what if you do want to specify the block as an argument? Or what if you want a way to reference a block to pass it to another method or generally manipulate it more easily? Enter…
Procs
Procs allow us to assign a block to an object:
awesome_proc = Proc.new { |x| puts x}
They also solve our above problem of being able to define a method that takes a block:
def run_a_block &my_proc
my_proc.call 'hello'
my_proc.call 'world'
end
What’s this? Why is there an ampersand now? That’s the explicit block we’ve
been eagerly awaiting. The ampersand encapsulates a block that’s being passed
into a proc for us, which we can then call using the call
method. This works
the same way as yield
. Now that we have a named proc, we have the ability to
reference it and pass it onto another method if we wish.
Note that if the method you write is expecting to be passed a proc — rather than a block — there’s no need for the ampersand. The ampersand bundles a block into a proc, or unbundles in the opposite direction.
Closures
Procs are a type of closure, so called because they enclose a piece of code along with its environment. Which may sound complicated, but is fairly straightforward in practice. Let’s look at an example:
greeting = "Hello from here"
printer = Proc.new { puts greeting }
def call_proc my_proc
greeting = "Hello from there"
my_proc.call
end
call_proc printer
You might expect that code to print “Hello from there”, but if you run it what you get is “Hello from here” because the proc has enclosed a reference to the context in which it was defined. If the value is changed in that context, it will also change for the proc. If it changes in a different context, it won’t.
Procs achieve this encapsulation by use of the Binding
class. This isn’t the
place to examine it in detail, but it’s worth knowing that this is what allows a
proc to “reach back” to the context in which it was defined.
Lambdas
In Ruby, a lambda is a specific type of proc. What makes them different from a
standard proc? The way they are defined, the way they handle return
and the
way they deal with arguments. Let’s look at each of those in turn.
awesome_lambda = -> { puts "I’m an awesome lambda" }
awesome_lambda.call
# Or, with arguments:
awesome_lambda = -> (name) { puts "Hello #{name}" }
So you can see here we use the ->
notation, which is arguably cleaner than
Proc.new
.
What’s different about their handling of return
? If you return from a lambda,
the lambda returns execution back to where it was called from. If you return
from a standard proc, however, it tries to return from the current context. To
make that a bit clearer, if a lambda calls return
, execution is returned to
the method which called it. If a standard proc calls return
, the method that
called it will itself return.
Finally, lambdas care about the number of arguments you define them with:
awesome_lambda = -> (x, y) { puts x, y }
awesome_proc = Proc.new { |x, y| puts x, y }
# These are all fine:
awesome_lambda(1, 2)
awesome_proc(1, 2)
awesome_proc(1)
# But this will cause an exception due to the incorrect number of arguments:
awesome_lambda(1)
As an aside, if you’re wondering why the odd-sounding name, it comes from the wider computer science concept of lambda expressions, which in turn take their name from the mathematical system of lambda calculus. This is so-called because of its use of the Greek letter lambda (λ) in its notation.
instance_exec
Finally, it’s worth making note of instance_exec
here. instance_exec
is a
way to call a block in the context of another block. It is by use of
instance_exec
that frameworks like RSpec and FactoryBot achieve their elegant,
clean syntax. If you want to read about it, Jason Swett has a great
post.
Conclusion
Blocks, procs and lambdas are a key part of Ruby and fully understanding how they work adds a powerful tool to your belt. The differences between standard procs and lambdas are subtle and could easily trip up the unsuspecting; I would suggest it’s worthwhile defaulting to the use of lambdas over procs due to their arguably more intuitive behaviour, and only switching to procs when you need their specific functionality.