Some time ago, as a dyed-in-the-wool C++ programmer who was exploring the mysterious world of Ruby, I got enthralled by the concept of closures and the way the language handled them. I was already familiar with the concept of closures from my JavaScript experience, but the manner in which Ruby handles closures both perplexed and fascinated me. Years later, I decided to write an article about them and here it is.
What’s a Closure?
A closure is basically a function that:
- Can be treated like a variable, i.e. assigned to another variable, passed as a method argument, etc.
- Remembers the values of all the variables that were in scope when the function was defined and is able to access these variables even if it is executed in a different scope.
Put differently, a closure is a first-class function that has lexical scope.
Code Blocks
A block is, in effect, an anonymous function that also offers closure-like functionality. Consider the following,
outer = 1
def m
inner = 99
puts "inner var = #{inner}"
end
Unlike some other languages, Ruby doesn’t nest scopes, so the inner and outer variables are totally shielded from each other. What if we want to access the inner variable from the outer (a.k.a main) scope? One way of doing it is by using a block:
outer = 1
def m
inner = 99
yield inner
puts "inner var = #{inner}"
end
m {|inner| outer += inner}
puts "outer var = #{outer}"
Output:
#=> inner var = 99
#=> outer var = 100
From within the method, we’re yielding the inner variable to a block that’s appended to our method call. We then use the inner variable value to do some arithmetic in our main scope. Althoh the code block is, effectively, an anonymous function, it can still access the variables in its surrounding scope, such as ‘outer’, while accessing variables within the method that yields it. In other words, using a block like that allows us to cross a scope gate.
“Why Can’t I Use the Method’s Return Value Instead?”
In this simple example, you could. However, if you want to return more than one variables, you fail, while you can easily yield
multiple variables to a block. More importantly, the method won’t have access to the surrounding variables at the point of its definition, so you won’t be able to do the cool things we’ll see in the rest of this article :)
Procs
You may have noticed that our code block example didn’t really fulfill the criteria we defined for a closure. While the block remembers variables in its surrounding scope and can access variables yielded to it from a different scope, we can’t pass it as a method argument or assign it to another object. In other words, blocks used with the yield statement are not true closures. Blocks can be true closures but they have to be treated as Procs. Let’s look at how we can do just that:
outer = 1
def m &a_block
inner = 99
a_block.call(inner)
puts "inner var = #{inner}"
puts "argument is a #{a_block.class}"
end
m {|inner| outer += inner}
puts "outer var = #{outer}"
Output:
#=> inner var = 99
#=> argument is a Proc
#=> outer var = 100
The first difference between this and our previous example is that we’re now defining a parameter for our method: &a_block
. This tells the method to expect a block as an argument and also treat it like a Proc
object (the & operator implicitly converts the block to a Proc
). A Proc is simple a named code block, meaning, an actual object. Since it is an object, the block can be passed around and have methods invoked on it. The method of interest here is #call
, which invokes the Proc. Knowing all this, we can rewrite our code as follows:
outer = 1
def m a_proc
inner = 99
a_proc.call(inner)
puts "inner var = #{inner}"
puts "argument is a #{a_proc.class}"
end
m proc {|inner| outer += inner}
# we can also use Proc.new instead of proc, with the same effect:
# m Proc.new {|inner| outer += inner}
puts "outer var = #{outer}"
Output:
#=> inner var = 99
#=> argument is a Proc
#=> outer var = 100
Here, we’re creating a Proc
object on the fly and passing it as a method argument. To fully leverage a closure, we need to be able to assign it to another variable and call it when we need it (deferred execution). Let’s modify our method to achieve just that:
outer = 1
def m a_var
inner = 99
puts "inner var = #{inner}"
proc {inner + a_var}
end
p = m(outer)
puts "p is a #{p.class}"
outer = 0
puts "changed outer to #{outer}"
puts "result of proc call: #{p.call}"
Output:
#=> inner var = 99
#=> p is a Proc
#=> changed outer to 0
#=> result of proc call: 100
Our method now receives the outer variable as an argument and returns the Proc
that does the addition of inner
and outer
. We then assign the Proc
to a variable (p
), called at our leisure further down in the code. Note that, even when we change the value of outer
before the proc call and set it to 0, our result isn’t affected. The Proc
remembers the value of outer
when it was defined, not when it was called. We now have a real born closure!
Lambdas
Time for a spot-the-difference game. Take a look at the following code and see how it differs from the code in the previous section:
outer = 1
def m a_var
inner = 99
puts "inner var = #{inner}"
lambda {inner + a_var}
end
p = m(outer)
puts "p is a #{p.class}"
outer = 0
puts "changed outer to #{outer}"
puts "result of proc call: #{p.call}"
Output:
#=> inner var = 99
#=> p is a Proc
#=> changed outer to 0
#=> result of proc call: 100
Yes, the only difference is that our method now returns a lambda instead of a proc. The functionality and output of our code remain exactly the same. But…hang on, we assigned a lambda to p
, butp
tells us it’s a Proc
! How’s that possible? The answer is: that lambda is but a Proc
in disguise. #lambda
is a Kernel
method that creates a Proc
object which behaves slightly differently to other Proc
objects.
“How Can I Tell If an Object is a proc or a lambda?”
Just ask it by calling its #lambda?
method.
obj = lambda {"hello"}.
puts obj.lambda?
#=> true
Also, calling #inspect
on a lambda will give you its class as a Proc(lambda)
.
Procs vs Lambdas
We already saw that a lambda is just a Proc
with some different behavior. The differences between a Proc
(proc) and a lambda encompass:
- Returning
- Argument checking
Returning from a Proc
def method_a
lambda { return "return from lambda" }.call
return "method a returns"
end
def method_b
proc { return "return from proc" }.call
return "method b returns"
end
puts method_a
puts method_b
Output:
#=> method a returns
#=> return from proc
While our lambda-using method (method_a
) behaves as expected, our proc-using method (method_b
) never gets to return, returning the Proc’s return value instead. There’s a simple explanation for this:
- A block created with lambda returns back to its parent scope just like methods do, so no surprises there.
A block created with
proc
(orProc.new
) thinks it’s part of its calling method, so returns back to its calling method’s parent scope. Which can be a bit shocking when you realize half of your method’s code wasn’t executed because theProc
you put half-way through it had areturn
statement. So, can we return from aProc
the “normal” way? Yes, if we use thenext
keyword, instead ofreturn
. We can re-]writemethod_b
so that it’s functionally the same tomethod_a
:def method_b proc { next "return from proc" }.call return "method b returns" end puts method_b
Output:
#=> method b returns
Argument Checking
Let’s create and call a lambda with two parameters:
l = lambda {|x, y| "#{x}#{y}"}
puts l.call("foo","bar")
Output:
#=> foobar
What happens if we omit an argument?
puts l.call("foo")
Output:
#=> wrong number of arguments (1 for 2) (ArgumentError)
That’s right, a lambda is strict about its arguments (arity), just like a method. What about procs?
p = proc {|x, y| "#{x}#{y}"}
puts p.call("foo","bar")
puts p.call("foo")
Output:
#=> foobar
#=> foo
We can see that the proc is much more chill about its arguments. If an argument’s missing, the proc will simply assume it’s nil
and get on with life.
Syntactic Sugar
One of the many things I love about Ruby is that it allows us to do the same thing in many different ways. Think that the #call
method is too Fortran-like for your tastes? No problem, call your proc with a dot or double-colon. Don’t like that either? There’s always the square bracket notation.
p = proc {|x, y| "#{x}#{y}"}
puts p.call("foo","bar")
puts p::("foo","bar")
puts p.("foo","bar")
puts p["foo","bar"]
Output:
#=> foobar
#=> foobar
#=> foobar
#=> foobar
Also, remember the unary ampersand operator (&
) we used earlier to convert a block to a proc? Well, it works the other way too. 1
p = proc {|i| i * 2}
l = proc {|i| i * 3}
puts [1,2,3].map(&p)
puts [1,2,3].map(&l)
Output:
#=> 2
#=> 4
#=> 6
#=> 3
#=> 6
#=> 9
Here, the #map
method expects a block, but we can easily pass it a proc or a lambda instead. This means we can utilize the power of closures to transpose variables from other scopes into the goodness of Ruby’s block-expecting methods, which is pretty powerful stuff.
So What’s the Big Deal?
Ruby offers unrivaled versatility in implementing closures. Blocks, procs, and lambdas can be used interchangeably to great effect. Much of the “magic” created by many of our favorite gems is facilitated by closures. Closures allow us to abstract our code in ways that make it smaller, tighter, re-usable and elegant 2. Ruby empowers developers with these wonderful and flexible constructs so that we can utilize closure power to the max.
[1]: Actually, the unary ampersand operator is nuanced in its own way
[2]: I can feel a new article coming up ;)
Frequently Asked Questions about Closures in Ruby
What is the significance of closures in Ruby?
Closures in Ruby are a fundamental concept that allows for higher-order functions and helps manage scope. They are essentially blocks of code that can be passed around as objects, retaining the variables from the context where they were defined. This makes them incredibly powerful for creating customizable functions, managing resources, and implementing patterns like iterators or callbacks.
How do closures differ from regular functions in Ruby?
Unlike regular functions, closures have the ability to capture or “close over” their surrounding context, including any variables that are in scope at the time the closure is created. This means that even if those variables go out of scope, the closure still has access to them. Regular functions, on the other hand, do not have this capability and can only access variables that are passed to them as arguments or are in their immediate scope.
Can you provide an example of a closure in Ruby?
Sure, here’s a simple example of a closure in Ruby:def make_multiplier(x)
return Proc.new {|y| x * y}
end
doubler = make_multiplier(2)
puts doubler.call(5) # Outputs: 10
In this example, make_multiplier
is a function that creates a new closure (a Proc
object) that multiplies its argument by x
. The doubler
closure retains the value of x
from when it was created, so it can still use it when it’s called later.
What are the different types of closures in Ruby?
Ruby has three types of closures: blocks, procs, and lambdas. Blocks are the simplest form and are typically used with methods like each
or map
. Procs are objects that encapsulate blocks of code, which can be stored in variables and passed around. Lambdas are a special type of proc that behave more like functions, in that they check the number of arguments and return control to the calling function when a return statement is executed.
How do I choose between a block, a proc, and a lambda?
The choice between a block, a proc, and a lambda depends on your specific needs. Blocks are the most common and are used for simple, one-off pieces of code. Procs are useful when you want to store a block for later use or pass it to another function. Lambdas are used when you need a closure that behaves more like a function, particularly in terms of argument handling and control flow.
Can closures in Ruby modify variables from their surrounding context?
Yes, closures in Ruby can modify variables from their surrounding context. This is known as variable mutation. It’s one of the features that make closures so powerful, but it can also lead to unexpected behavior if not used carefully.
How does variable scope work with closures in Ruby?
When a closure is defined, it captures the current context, including any local variables that are in scope. These variables remain accessible to the closure even after they go out of scope in the surrounding code. However, if a variable with the same name is defined inside the closure, it will shadow the captured variable.
Can I use closures with Ruby’s built-in methods?
Absolutely! Many of Ruby’s built-in methods, particularly those for handling collections like arrays and hashes, accept blocks as arguments. These blocks are essentially closures, and they allow you to customize the behavior of the method for your specific needs.
What is a closure’s arity in Ruby?
A closure’s arity is the number of arguments it expects. You can check a closure’s arity in Ruby by calling the arity
method on it. This can be particularly useful with lambdas, which check the number of arguments and raise an error if the number of arguments doesn’t match the arity.
Can I return a closure from a function in Ruby?
Yes, you can return a closure from a function in Ruby. This is a powerful feature that allows you to create factory functions, which can generate customized functions on the fly. The returned closure will retain any variables from the function’s scope, allowing it to have state and behavior that’s customized to the specific call.
Fred is a software jack of all trades, having worked at every stage of the software development life-cycle. He loves: solving tricky problems, Ruby, Agile methods, meta-programming, Behaviour-Driven Development, the semantic web. Fred works as a freelance developer, and consultant, speaks at conferences and blogs.