Closures in Ruby

Share this article

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:

  1. Can be treated like a variable, i.e. assigned to another variable, passed as a method argument, etc.
  2. 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:

  1. Returning
  2. 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:

  1. A block created with lambda returns back to its parent scope just like methods do, so no surprises there.
  2. A block created with proc (or Proc.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 the Proc you put half-way through it had a return statement. So, can we return from a Proc the “normal” way? Yes, if we use the next keyword, instead of return. We can re-]write method_b so that it’s functionally the same to method_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 HeathFred Heath
View Author

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.

GlennG
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form