wmock
wmock

Reputation: 5492

Understanding Ruby Closures

I'm trying to better understand Ruby closures and I came across this example code which I don't quite understand:

def make_counter
  n = 0
  return Proc.new { n = n + 1 }
end

c = make_counter
puts c.call # => this outputs 1
puts c.call # => this outputs 2

Can someone help me understand what actually happens in the above code when I call c = make_counter? In my mind, here's what I think is happening:

Ruby calls the make_counter method and returns a Proc object where the code block associated with the Proc will be { n = 1 }. When the first c.call is executed, the Proc object will execute the block associated with it, and returns n = 1. However, when the second c.call is executed, doesn't the Proc object still execute the block associated with it, which is still { n = 1 }? I don't get why the output will change to 2.

Maybe I'm not understanding this at all, and it would be helpful if you could provide some clarification on what's actually happening within Ruby.

Upvotes: 3

Views: 2628

Answers (3)

Daniel
Daniel

Reputation: 15413

I always feel like to understand whats going on, its always important to revisit the basics. No one ever answered the question of what is a Proc in Ruby which to a newbie reading this post, that would be crucial and would help in answering this question.

At a high-level, procs are methods that can be stored inside variables.

Procs can also take a code block as its parameter, in this case it took n = n + 1. In other programming languages a block is called a closure. Blocks allow you to group statements together and encapsulate behavior.

There are two ways to create blocks in Ruby. The example you provide is using curly braces syntax.

So why use Procs if you can use methods to perform the same functionality?

The answer is that Procs give you more flexibility than methods. With Procs you can store an entire set of processes inside a variable and then call the variable anywhere else in your program.

In this case, Proc was written inside a method and then that method was stored inside a variable called c and then called with puts each time incrementing the value of n.

Similar to Procs, Lambdas also allow you to store functions inside a variable and call the method from other parts of a program.

Upvotes: 1

Daniel Viglione
Daniel Viglione

Reputation: 9407

This here:

return Proc.new { n = n + 1 }

Actually, returns a proc object which has a block associated with it. And Ruby creates a binding with blocks! So the execution context is stored for later use and hence why we can increment n. Let me go a bit further into explaining Ruby Closures, so you can have a more broader idea.

First, we need to clarify the technical term 'binding'. In Ruby, a binding object encapsulates the execution context at some particular scope in a program and retains this context for future use in the program. This execution context includes arguments passed to a method and any local variables defined in the method, any associated blocks, the return stack and the value of self. Take this example:

class SomeClass 
  def initialize
    @ivar = 'instance variable'
  end

  def m(param)
    lvar = 'local variable'
    binding
  end
end


b = SomeClass.new.m(100) { 'block executed' }
 => #<Binding:0x007fb354b7aca0>

eval "puts param", b
=> 100
eval "puts lvar", b
=> local variable
eval "puts yield", b
=> block executed
eval "puts self", b
=> #<SomeClass:0x007fb354ad82e8>
eval "puts @ivar", b
instance variable

The last statement might seem a little tricky but it's not. Remember binding holds execution context for later use. So when we invoke yield, it is invoking yield as if it was still in that execution context and hence it invokes the block.

It's interesting, you can even reassign the value of the local variables in the closure:

eval "lvar = 'changed in eval'", b
eval "puts lvar", b
=> changed in eval

Now this is all cute, but not so useful. Bindings are really useful as it pertains to blocks. Ruby associates a binding object with a block. So when you create a proc or a lambda, the resulting Proc object holds not just the executable block but also bindings for all the variables used by the block.

You already know that blocks can use local variables and method arguments that are defined outside the block. In the following code, for example, the block associated with the collect iterator uses the method argument n:

# multiply each element of the data array by n
def multiply(data, n)
 data.collect {|x| x*n }
end
puts multiply([1,2,3], 2) # Prints 2,4,6

What is more interesting is that if the block were turned into a proc or lambda, it could access n even after the method to which it is an argument had returned. That's because there is a binding associated to the block of the lambda or proc object! The following code demonstrates:

# Return a lambda that retains or "closes over" the argument n
def multiplier(n)
 lambda {|data| data.collect{|x| x*n } }
end
doubler = multiplier(2) # Get a lambda that knows how to double
puts doubler.call([1,2,3]) # Prints 2,4,6

The multiplier method returns a lambda. Because this lambda is used outside of the scope in which it is defined, we call it a closure; it encapsulates or “closes over” (or just retains) the binding for the method argument n.

It is important to understand that a closure does not just retain the value of the variables it refers to—it retains the actual variables and extends their lifetime. Another way to say this is that the variables used in a lambda or proc are not statically bound when the lambda or proc is created. Instead, the bindings are dynamic, and the values of the variables are looked up when the lambda or proc is executed.

Upvotes: 0

PinnyM
PinnyM

Reputation: 35533

The block is not evaluated when make_counter is called. The block is evaluated and run when you call the Proc via c.call. So each time you run c.call, the expression n = n + 1 will be evaluated and run. The binding for the Proc will cause the n variable to remain in scope since it (the local n variable) was first declared outside the Proc closure. As such, n will keep incrementing on each iteration.

To clarify this further:

  • The block that defines a Proc (or lambda) is not evaluated at initialization - the code within is frozen exactly as you see it.
  • Ok, the code is actually 'evaluated', but not for the purpose of changing the frozen code. Rather, it is checked for any variables that are currently in scope that are being used within the context of the Proc's code block. Since n is a local variable (as it was defined the line before), and it is used within the Proc, it is captured within the binding and comes along for the ride.
  • When the call method is called on the Proc, it will execute the 'frozen' code within the context of that binding that had been captured. So the n that had been originally been assigned as 0, is incremented to 1. When called again, the same n will increment again to 2. And so on...

Upvotes: 8

Related Questions