Reputation: 5492
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
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 Proc
s, Lambdas
also allow you to store functions inside a variable and call the method from other parts of a program.
Upvotes: 1
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
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:
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.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