Friday, January 8, 2010

Section 6.6. Closures








6.6. Closures


In Ruby, procs and lambdas are
closures. The term "closure" comes from the early
days of computer science; it refers to an object that is both an invocable
function and a variable binding for that function. 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, and possibly even surprising, 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. 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.



6.6.1. Closures and Shared Variables


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.


As an example, the following code defines a method that returns
two lambdas. Because the lambdas are defined in the same scope, they
share access to the variables in that scope. When one lambda alters the
value of a shared variable, the new value is available to the other
lambda:


# Return a pair of lambdas that share access to a local variable.
def accessor_pair(initialValue=nil)
value = initialValue # A local variable shared by the returned lambdas.
getter = lambda { value } # Return value of local variable.
setter = lambda {|x| value = x } # Change value of local variable.
return getter,setter # Return pair of lambdas to caller.
end

getX, setX = accessor_pair(0) # Create accessor lambdas for initial value 0.
puts getX[] # Prints 0. Note square brackets instead of call.
setX[10] # Change the value through one closure.
puts getX[] # Prints 10. The change is visible through the other.





The fact that lambdas created in the same scope share access to
variables can be a feature or a source of bugs. Any time you have a
method that returns more than one closure, you should pay particular
attention to the variables they use. Consider the following code:


# Return an array of lambdas that multiply by the arguments
def multipliers(*args)
x = nil
args.map {|x| lambda {|y| x*y }}
end

double,triple = multipliers(2,3)
puts double.call(2) # Prints 6 in Ruby 1.8



This multipliers method uses the
map iterator and a block to return an array of
lambdas (created inside the block). In Ruby 1.8, block arguments are not
always local to the block (see Section 5.4.3), and
so all of the lambdas that are created end up sharing access to
x, which is a local variable of the
multipliers method. As noted above, closures don't
capture the current value of the variable: they capture the variable
itself. Each of the lambdas created here share the variable
x. That variable has only one value, and all of the
returned lambdas use that same value. That is why the lambda we name
double ends up tripling its argument instead of
doubling it.


In this particular code, the issue goes away in Ruby 1.9 because
block arguments are always block-local in that version of the language.
Still, you can get yourself in trouble any time you create lambdas
within a loop and use a loop variables (such as an array index) within
the lambda.




6.6.2. Closures and Bindings


The Proc class defines a method named binding. Calling this
method on a proc or lambda returns a Binding object
that represents the bindings in effect for that closure.



More About Bindings


We've been discussing the bindings of a closure as if they were
simply a mapping from variable names to variable values. In fact,
bindings involve more than just
variables. They hold all the information necessary to execute a
method, such as the value of self, and the block,
if any, that would be invoked by a yield.




A Binding object doesn't have interesting
methods of its own, but it can be used as the second argument to the
global eval function (see Section 8.2),
providing a context in which to evaluate a string of Ruby code.
In Ruby 1.9, Binding has its own
eval method, which you may prefer to use. (Use
ri to learn more about Kernel.eval
and Binding.eval.)


The use of a Binding object and the
eval method gives us a back door through which we can
manipulate the behavior of a closure. Take another look at this code
from earlier:


# 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



Now suppose we want to alter the behavior of
doubler:


eval("n=3", doubler.binding) # Or doubler.binding.eval("n=3") in Ruby 1.9
puts doubler.call([1,2,3]) # Now this prints 3,6,9!



As a shortcut, the eval method allows you to
pass a Proc object directly instead of passing the
Binding object of the Proc. So we
could replace the eval invocation above with:


eval("n=3", doubler)



Bindings are not only a feature of closures. The
Kernel.binding method returns a Binding object that represents the
bindings in effect at whatever point you happen to call it.










No comments: