程式扎記: [ In Action ] Working with closures - Understanding scoping

標籤

2014年2月9日 星期日

[ In Action ] Working with closures - Understanding scoping

Preface: 
You have seen how to create closures when they are needed for a method call and how to work with closures when they are passed to your method. This is very powerful while still simple to use. This section looks under the hood and deepens your understanding of what happens when you use this simple construction. We explore what data and methods you can access from a closure, what difference using the this reference makes, and how to put your knowledge to the test with a classic example designed to test any language’s expressiveness. 

This is a bit of a technical section, and you can safely skip it on first read. However, at some point you may want to read it and learn how Groovy can provide all those clever tricks. In fact, knowing the details will enable you to come up with particularly elegant solutions yourself. 

What is available inside a closure is called its scope. The scope defines: 
■ What local variables are accessible
■ What this (the current object) refers to
■ What fields and methods are accessible

We start with an explanation of the behavior that you have seen so far. For that purpose, we revisit a piece of code that does something 10 times: 
  1. def x = 0           
  2. 10.times {       
  3.     x++         
  4. }               
  5. assert x == 10  
It is evident that the closure that is passed into the times method can access variable x , which is locally accessible when the closure is declared. Remember: The curly braces show the declaration time of the closure, not the execution timeThe closure can access x for both reading and writing at declaration time. 

This leads to a second thought: The closure surely needs to also access x at execution time. How could it increment it otherwise? But the closure is passed to the timesmethod, a method that is called on the Integer object with value 10 . That method, in turn, calls back to our closure. But the times method has no chance of knowing about x . So it cannot pass it to the closure, and it surely has no means of finding out what the closure is doing with it. 

The only way in which this can possibly work is if the closure somehow remembers the context of its birth and carries it along throughout its lifetime. That way, it can work on that original context whenever the situation calls for it. This birthday context that the closure remembers needs to be a reference, not a copy. If that context were a copy of the original one, there would be no way of changing the original from inside the closure. But our example clearly does change the value of x —otherwise the assertion would fail. Therefore, the birthday context must be a reference. 

The simple variable scope: 
Figure 5.3 depicts your current understanding of which objects are involved in the times example and how they reference each other. 
 

The Script creates a Closure that has a back reference to x , which is in the local scope of its declarer. Script calls the times method on the Integer 10 object, passing the declared closure as a parameter. In other words, when times is executed, a reference to the closure object lies on the stack. The times method uses this reference to execute Closure ’s call method, passing its local variable count to it. In this specific example, the count is not used within Closure.call. Instead, Closure.call only works on the x reference that it holds to the local variable x in Script

The general closure scope: 
It would not be surprising if other scope elements were treated the same as local variables: the value of this , fields, methods, and parameters. 

This generalization is correct, but the this reference is a special case. Inside a closure, you could legitimately assume that this would refer to the current object, which is the closure object itself. On the other hand, it should make no difference whether you use this.reference or plain reference for locally accessible references. The first approach has long been used in Groovy but was changed in favor of the latter by the JSR expert group. 

A reference to the declaring object is held in a special variable called owner. Listing 5.8 extends the purpose of the initial example to reveal the remaining scope elements. 
- Listing 5.8 Investigating the closure scope 
  1. class Mother {  
  2.     int field = 1  
  3.     int foo(){  
  4.         return 2  
  5.     }  
  6.     // 1) This method creates and returns the closure  
  7.     Closure birth (param) {     
  8.         def local = 3  
  9.         def closure = { caller ->  
  10.             [this, field, foo(), local, param, caller, owner]   
  11.         }  
  12.         return closure  
  13.     }  
  14. }                      
  15. Mother julia = new Mother()  
  16. closure = julia.birth(4)            // 2) Let a mother give birth to a closure  
  17. context = closure.call(this)        // 3) Call the closure  
  18.   
  19. println context[0].class.name       // 4) Mother  
  20. assert context[1..4] == [1,2,3,4]   // 5) No surprise?  
  21. assert context[5instanceof Script // 6) The calling object  
  22. assert context[6instanceof Mother // 7) The declaring object  
  23.   
  24. firstClosure  = julia.birth(4)  
  25. secondClosure = julia.birth(4)  
  26.   
  27. // 8) Closure braces are like new  
  28. assert false == firstClosure.is(secondClosure)  
We implement a small class Mother that should give birth to a closure through a method with that name. The class has a field, another method, parameters, and local variables that we can study. The closure should return a list of all elements that are in the current context (aka scope). Behind the scenes, these elements will be bound at declaration time but not evaluated until the closure is called. At (4) the example should print the Mother class name by the time you are reading this. Groovy versions before 1.0 printed the closure type. Figure 5.4 shows who refers to whom in listing 5.8. 
 

Scoping at work: the classic accumulator test 
There is a classic example to compare the power of languages by the way they support closures. One of the things it highlights is the power of the scoping rules for those languages as they apply to closures. Paul Graham first proposed this test in his excellent article “Revenge of the Nerds” (http://www.paulgraham.com/icad.html). Beside the test, his article is very interesting and informative to read. It talks about the difference a language can make. You will find good arguments in it for switching to Groovy.

In some languages, this test leads to a brain-teasing solution. Not so in Groovy. The Groovy solution is exceptionally obvious and straightforward to achieve. 

Here is the original requirement statement: 
"We want to write a function that generates accumulators—a function that takes a number n, and returns a function that takes another number i and returns nincremented by i."

The following are proposed solutions for other languages: 
In Lisp: 
  1. (defun foo (n)  
  2.   (lambda (i) (incf n i)))  
In Perl 5: 
  1. sub foo {    
  2.   my ($n) = @_;  
  3.   sub {$n += shift}  
  4. }  
The following steps lead to a Groovy solution, as shown in listing 5.9: 
1. We need a function that returns a closure. 
In Groovy, we don’t have functions, but methods. (Actually, we have not only methods, but also closures. But let’s keep it simple.) We use def to declare such a method. It has only one line, which after return creates a new closure. We will call this method foo to make the solutions comparable in size. The namecreateAccumulator would better reflect the purpose.

2. Our method takes an initial value n as required. 
Because n is a parameter to the method that declares the closure, it gets bound to the closure scope. We can use it inside the closure body to calculate the incremented value.

3. The incremented value is not only calculated but also assigned to n as the new value. That way we have a true accumulation. 

We add a few assertions to verify our solution and reveal how the accumulator is supposed to be used. Listing 5.9 shows the full code. 
- Listing 5.9 The accumulator problem in Groovy 
  1. def foo(n) {  
  2.     return {n += it}  
  3. }  
  4. def accumulator =  foo(1)  
  5. assert accumulator(2) == 3  
  6. assert accumulator(1) == 4  
Is this test of any practical relevance? Maybe not in the sense that we would ever need an accumulator generator, but it is in a different sense. Passing this test means that the language is able to dynamically put logic in an object and manage the context that this object lives in. This is an indication of how powerful abstractions in that language can be. 

Supplement: 
Working with closures - Returning & Support for design patterns 
[ User Guide ] Closures

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!