Understanding Ruby Proc objects

by Jason Swett,

What we’re going to do and why

If you’re a Ruby programmer, you almost certainly use Proc objects all the time, although you might not always be consciously aware of it. Blocks, which are ubiquitous in Ruby, and lambdas, which are used for things like Rails scopes, both involve Proc objects.

In this post we’re going to take a close look at Proc objects. First we’ll do a Proc object “hello world” to see what we’re dealing with. Then we’ll unpack the definition of Proc objects that the official Ruby docs give us. Lastly we’ll see how Proc objects relate to other concepts like blocks and lambdas.

A Proc object “hello world”

Before we talk about what Proc objects are and how they’re used, let’s take a look at a Proc object and mess around with it a little bit, just to see what one looks like.

The official Ruby docs provide a pretty good Proc object “hello world” example:

square = Proc.new { |x| x**2 }

We can see how this Proc object works by opening up an irb console and defining the Proc object there.

> square = Proc.new { |x| x**2 }
 => #<Proc:0x00000001333a8660 (irb):1> 
> square.call(3)
 => 9 
> square.call(4)
 => 16 
> square.call(5)
 => 25

We can kind of intuitively understand how this works. A Proc object behaves somewhat like a method: you define some behavior and then you can use that behavior repeatedly wherever you want.

Now that we have a loose intuitive understanding, let’s get a firmer grasp on what Proc objects are all about.

Understanding Proc objects more deeply

The official Ruby docs’ definition of Proc objects

According to the official Ruby docs on Procs objects, “a Proc object is an encapsulation of a block of code, which can be stored in a local variable, passed to a method or another Proc, and can be called.”

This definition is a bit of a mouthful. When I encounter wordy definitions like this, I like to separate them into chunks to make them easier to understand.

The Ruby Proc object definition, broken into chunks

A Proc object is:

  • an encapsulation of a block of code
  • which can be stored in a local variable
  • or passed to a method or another Proc
  • and can be called

Let’s take these things one-by-one.

A Proc object is an encapsulation of a block of code

What could it mean for something to be an encapsulation of a block of code? In general, when you “encapsulate” something, you metaphorically put it in a capsule. Things that are in capsules are isolated from whatever’s on the outside of the capsule. Encapsulating something also implies that it’s “packaged up”.

So when the docs say that a Proc object is “an encapsulation of a block of code”, they must mean that the code in a Proc object is packaged up and isolated from the code outside it.

A Proc object can be stored in a local variable

For this one let’s look at an example, straight from the docs:

square = Proc.new { |x| x**2 }

As we can see, this piece of code creates a Proc object and stores it in a local variable called square. So this part of the definition, that a Proc object can be stored in a local variable, seems easy enough to understand.

A Proc object can be passed to a method or another Proc

This one’s a two-parter so let’s take each part individually. First let’s focus on “A Proc object can be passed to another method”.

Here’s a method which can accept a Proc object. The method is followed by the definition of two Proc objects: square, which squares whatever number you give it, and double, which doubles whatever number you give it.

def perform_operation_on(number, operation)
  operation.call(number)
end

square = Proc.new { |x| x**2 }
double = Proc.new { |x| x * 2 }

puts perform_operation_on(5, square)
puts perform_operation_on(5, double)

If you were to run this code you would get the following output:

25
10

So that’s what it means to pass a Proc object into a method. Instead of passing data as a method argument like normal, you can pass behavior. Or, to put it another way, you can pass an encapsulation of a block of code. It’s then up to that method to execute that encapsulated block of code whenever and however it sees fit.

If we want to pass a Proc object into another Proc object, the code looks pretty similar to our other example above.

perform_operation_on = Proc.new do |number, operation|
  operation.call(number)
end

square = Proc.new { |x| x**2 }
double = Proc.new { |x| x * 2 }

puts perform_operation_on.call(5, square)
puts perform_operation_on.call(5, double)

The only difference between this example and the one above it is that, in this example, perform_operation_on is defined as a Proc object rather than a method. The ultimate behavior is exactly the same though.

A Proc object can be called

This last part of the definition of a Proc object, “a Proc object can be called”, is perhaps obvious at this point but let’s address it anyway for completeness’ sake.

A Proc object can be called using the #call method. Here’s an example.

square = Proc.new { |x| x**2 }
puts square.call(3)

There are other ways to call a Proc object but they’re not important for understanding Proc objects conceptually.

Closures

In order to fully understand Proc objects, we need to understand something called closures. The concept of a closure is a broader concept that’s not unique to Ruby.

You might have heard about closures. I personally have always thought of closures as some fancy computer-sciencey concept that must be hard to understand.

But if you strip away the jargon, there’s not a lot of complexity left. Here’s my definition of closures: a closure is a record which stores a function plus (potentially) some variables.

Ruby Proc objects are closures. In other words, when a Proc object is defined, not only does the behavior of the Proc object get saved, but any variables that are mixed in come along for the ride too.

Here’s an example. (This is example is based on an example from the Ruby docs.)

def multiplication_proc(factor)
  Proc.new { |n| n * factor }
end

multiply_by_3 = multiplication_proc(3)
multiply_by_5 = multiplication_proc(5)

puts multiply_by_3.call(10) # 30
puts multiply_by_5.call(10) # 50

When we generate a “multiply by 3” Proc object by calling multiplication_proc(3), the 3 gets “baked in” to the Proc object that multiplication_proc(3) returns. Thus, when we do multiply_by_3.call(10) later, our Proc object multiplies 10 by 3, returning 30. Same deal with multiply_by_5.

So, remember:

  • A closure is a record which stores a function plus some variables.
  • A Ruby Proc object is a closure.

Proc objects and blocks

Every block in Ruby is a Proc object, loosely speaking. Here’s a custom method that accepts a block as an argument.

def my_method(&block)
  puts block.class
end

my_method { "hello" }

If you were to run the above code, the output would be:

Proc

That’s because the block we passed when calling my_method is a Proc object.

Below is an example that’s functionally equivalent to the above. The & in front of my_proc converts the Proc object into a block.

def my_method(&block)
  puts block.class
end

my_proc = Proc.new { "hello" }
my_method &my_proc

Proc objects and lambdas

Lambdas are also Proc objects. This can be proven by running the following in an irb console:

> my_lambda = lambda { |x| x**2 }
 => #<Proc:0x00000001241e82a8 (irb):1 (lambda)> 
> my_lambda.class
 => Proc

The difference between lambdas and Proc objects is that the two have certain subtle differences in behavior. For example, in lambdas, return means “exit from this lambda”. In regular Proc objects, return means “exit from embracing method”. I won’t go into detail on the differences between lambdas and Proc objects because it’s outside the scope of what I’m trying to convey in this post. Instead you can read these details in the official docs.

Takeaways

  • A Proc object is an encapsulation of a block of code, which can be stored in a local variable, passed to a method or another Proc, and can be called.
  • A closure is a record which stores a function plus some variables. Proc objects are closures.
  • Blocks are Proc objects.
  • Lambdas are Proc objects too, although a special kind of Proc object with subtly different behavior.

2 thoughts on “Understanding Ruby Proc objects

Leave a Reply

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