Understanding Ruby closures

by Jason Swett,

Why you’d want to know about Ruby closures

One of the areas of Ruby that’s simultaneously one of the most fundamental parts of the language but perhaps one of the hardest to understand is blocks.

Ruby functions like map and each operate using blocks. You’ll also find heavy use of blocks in popular Ruby libraries including Ruby on Rails itself.

If you start to dig into Ruby blocks, you’ll discover that, in order to understand blocks, you have to understand something else called Proc objects.

And as if that weren’t enough, you’ll then discover that if you want to deeply understand Proc objects, you’ll have to understand closures.

The concept of a closure is one that suffers from 1) an arguably misleading name (more about this soon) and 2) unhelpful, jargony explanations online.

My goal with this post is to provide an explanation of closures in plain language that can be understood by someone without a Computer Science background. And in fact, a Computer Science background is not needed, it’s only the poor explanations of closures that make it seem so.

Let’s dig deeper into what a closure actually is.

What a closure is

A closure is a record which stores a function plus (potentially) some variables.

I’m going to break this definition into parts to reduce the chances that any part of it is misunderstood.

  • A closure is a record
  • which stores a function
  • plus (potentially) some variables

I’m going to discuss each part of this definition individually.

First, a reminder: the whole reason we’re interested in Ruby closures is because of the Ruby concept called a Proc object, which is heavily involved in blocks. A Proc object is a closure. Therefore, all the examples of closures in this post will take the form of Proc objects.

If you’re not yet familiar with Proc objects, I would suggest taking a look at my other post, Understanding Ruby Proc objects, before continuing. It will help you understand the ideas in this post better.

First point: “A closure is a record”

A closure is a value that can be assigned to a variable or some other kind of “record”. The term “record” doesn’t have a special technical meaning here, we just use the word “record” because it’s broader than “variable”. Shortly we’ll see an example of a closure being assigned to something other than a variable.

Here’s a Proc object that’s assigned to a variable.

my_proc = Proc.new { puts "I'm in a closure!" }
my_proc.call

Remember that every Proc object is a closure. When we do Proc.new we’re creating a Proc object and thus a closure.

Here’s another closure example. Here, instead of assigning the closure to a variable, we’re assigning the closure to a key in a hash. The point here is that the thing a closure gets assigned to isn’t always a variable. That’s why we say “record” and not “variable”.

my_stuff = { my_proc: Proc.new { puts "I'm in a closure too!" } }
my_stuff[:my_proc].call

Second point: “which stores a function”

As you may have deduced, or as you may have already known, the code between the braces (puts "I'm in a closure!") is the function we’re talking about when we say “a closure is a record which stores a function”.

my_proc = Proc.new { puts "I'm in a closure!" }
my_proc.call

A closure can be thought of as a function “packed up” into a variable. (Or, more precisely, a variable or some other kind of record.)

Third point: “plus (potentially) some variables”

Here’s a Proc object (which, remember, is a closure) that involves an outside variable.

The variable number_of_exclamation_points gets included in the “environment” of the closure. Each time we call the closure that we’ve named amplifier, the number_of_exclamation_points variable gets incremented and one additional exclamation point gets added to the string that gets outputted.

number_of_exclamation_points = 0

amplifier = Proc.new do
  number_of_exclamation_points += 1
  "louder" + ("!" * number_of_exclamation_points)
end

puts amplifier.call # louder!
puts amplifier.call # louder!!
puts amplifier.call # louder!!!
puts amplifier.call # louder!!!!
puts number_of_exclamation_points # 4 - the original variable was mutated

As a side note, I find the name “closure” to be misleading. The fact that the above closure can mutate number_of_exclamation_points, a variable outside the function’s scope, seems to me like a decidedly un-closed idea. In fact, it seems like there’s a tunnel, an opening, between the closure and the outside scope, through which changes can leak.

I personally started having an easy time understanding closures once I stopped trying to connect the idea of “a closed thing” with the mechanics of how closures actually work.

Takeaways

  • Ruby blocks heavily involve Proc objects.
  • Every Proc object is a closure.
  • A closure is a record which stores a function plus (potentially) some variables.

Leave a Reply

Your email address will not be published. Required fields are marked *