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:
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:
- def x = 0
- 10.times {
- x++
- }
- assert x == 10
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
- class Mother {
- int field = 1
- int foo(){
- return 2
- }
- // 1) This method creates and returns the closure
- Closure birth (param) {
- def local = 3
- def closure = { caller ->
- [this, field, foo(), local, param, caller, owner]
- }
- return closure
- }
- }
- Mother julia = new Mother()
- closure = julia.birth(4) // 2) Let a mother give birth to a closure
- context = closure.call(this) // 3) Call the closure
- println context[0].class.name // 4) Mother
- assert context[1..4] == [1,2,3,4] // 5) No surprise?
- assert context[5] instanceof Script // 6) The calling object
- assert context[6] instanceof Mother // 7) The declaring object
- firstClosure = julia.birth(4)
- secondClosure = julia.birth(4)
- // 8) Closure braces are like new
- assert false == firstClosure.is(secondClosure)
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:
The following are proposed solutions for other languages:
In Lisp:
- (defun foo (n)
- (lambda (i) (incf n i)))
- sub foo {
- my ($n) = @_;
- sub {$n += shift}
- }
1. We need a function that returns a closure.
2. Our method takes an initial value n as required.
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
- def foo(n) {
- return {n += it}
- }
- def accumulator = foo(1)
- assert accumulator(2) == 3
- assert accumulator(1) == 4
Supplement:
* Working with closures - Returning & Support for design patterns
* [ User Guide ] Closures
沒有留言:
張貼留言