Ruby
Article

Closures in Ruby

By Fred Heath

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 ;)

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in Ruby, once a week, for free.