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

標籤

2014年2月9日 星期日

[ In Action ] Working with closures - Using closures

Preface: 
So far, you have seen how to declare a closure for the purpose of passing it for execution, to the each method for example. But what happens inside the each method? How does it call your closure? If you knew this, you could come up with equally smart implementations. We’ll first look at how simple calling a closure is and then move on to explore some advanced methods that the Closure type has to offer. 

Calling a closure: 
Suppose we have a reference x pointing to a closure; we can call it with x.call() or simply x() . You have probably guessed that any arguments to the closure call go between the parentheses. We start with a simple example. Listing 5.5 shows the same closure being 
called both ways. 
- Listing 5.5 Calling closures 
  1. def adder = { x, y -> return x+y }               
  2. assert adder(43) == 7                          
  3. assert adder.call(26) == 8  
We start off by declaring pretty much the simplest possible closure—a piece of code that returns the sum of the two parameters it is passed. Then we call the closure both directly and using the call method. Both ways of calling the closure achieve exactly the same effect. 

Now let’s try something more involved. In listing 5.6, we demonstrate calling a closure from within a method body and how the closure gets passed into that method in the first place. The example measures the execution time of the closure. 
- Listing 5.6 Calling closures 
  1. def benchmark(repeat, Closure worker){  // 1) Put closures last    
  2.     start = System.currentTimeMillis()  // 2) Some pre-work  
  3.     repeat.times{worker(it)}            // 3) Call closure the given number of times  
  4.     stop = System.currentTimeMillis()   // 4) Some post-work  
  5.     return stop - start  
  6. }  
  7.   
  8. // 5) Pass different closures for analysis  
  9. slow = benchmark(10000) { (int) it / 2 }  
  10. fast = benchmark(10000) { it.intdiv(2) }  
  11. assert fast * 2 < slow  
BY THE WAY. 
This kind of benchmarking should not be taken too seriously. There are all kinds of effects that can heavily influence such wall-clock based measurements: machine characteristics, operating system, current machine load, JDK version, Just-In-time compiler and Hotspot settings, and so on

Figure 5.2 shows the UML sequence diagram for the general calling scheme of the declaring object that creates the closure, the method invocation on the caller, and the caller’s callback to the given closure. 
 

When calling a closure, you need to pass exactly as many arguments to the closure as it expects to receive, unless the closure defines default values for its parameters. This default value is used when you omit the corresponding argument. The following is a variant of the addition closure as used in listing 5.5, with a default value for the second parameter and two calls—one that passes two arguments, and one that relies on the default: 
  1. def adder = { x, y=5 -> return x+y }  
  2. assert adder(43) == 7     
  3. assert adder.call(7) == 12  
For the use of default parameters in closures, the same rules apply as for default parameters for methods. Also, closures can be used with a parameter list of variable length in the same way that methods can. We will cover this in section 7.1.2. 

More closure methods: 
The class groovy.lang.Closure is an ordinary class, albeit one with extraordinary power and extra language support. It has various methods available beyond call . We will present the most the important ones—even though you will usually just declare and call closures, it’s nice to know there’s some extra power available when you need it. 

Reacting on the parameter count 
A simple example of how useful it is to react on the parameter count of a closure is map ’s each method, which we discussed in section 4.3.2. It passes either a Map.Entryobject or key and value separately into the given closure, depending on whether the closure takes one or two arguments. You can retrieve the information about expected parameter count (and types, if declared) by calling closure’s getParameterTypes method: 
  1. def caller (Closure closure){  
  2.     closure.getParameterTypes().size()  
  3. }  
  4. assert caller { one -> }      == 1  
  5. assert caller { one, two -> } == 2  
As in the Map.each example, this allows for the luxury of supporting closures with different parameter styles, adapted to the caller’s needs. 

How to curry favor with a closure 
Currying is a technique invented by Moses Schönfinkel and Gottlob Frege, and named after the logician Haskell Brooks Curry (1900..1982), a pioneer in functional programming. (Unsurprisingly, the functional language Haskell is also named after Curry.) The basic idea is to take a function with multiple parameters and transform it into a function with fewer parameters by fixing some of the values. A classic example is to choose some arbitrary value n and transform a function that sums two parameters into a function that takes a single parameter and adds n to it. 

In Groovy, Closure ’s curry method returns a clone of the current closure, having bound one or more parameters to a given value. Parameters are bound to curry ’s arguments from left to right. Listing 5.7 gives an implementation. 
- Listing 5.7 A simple currying example 
  1. def adder = {x, y -> return x+y}  
  2. def addOne = adder.curry(1)  
  3. assert addOne(5) == 6  
We reuse the same closure you’ve seen a couple of times now for general summation. We call the curry method on it to create a new closure, which acts like a simple adder, but with the value of the first parameter always fixed as 1. Finally, we check our results. 

The real power of currying comes when the closure’s parameters are themselves closures. This is a common construction in functional programming, but it does take a little getting used to. For an example, suppose you are implementing a logging facility. It should support filtering of log lines, formatting them, and appending them to an output device. Each activity should be configurable. The idea is to provide a single closure for a customized version of each activity, while still allowing you to implement the overall pattern of when to apply a filter, do the formatting, and finally output the log line in one place. The following shows how currying is used to inject the customized activity into that pattern: 
  1. // 1) Configuration use  
  2. def configurator = { format, filter, line ->     
  3.     filter(line) ?  format(line) : null                              
  4. }  
  5.   
  6. // 2) Formatting use  
  7. def appender = { config, append, line ->                           
  8.     def out = config(line)                   
  9.     if (out) append(out)                     
  10. }  
  11.   
  12. // 3) Filter, format, and output parts  
  13. def dateFormatter   = { line -> "${new Date()}: $line" }     
  14. def debugFilter     = { line -> line.contains('debug') }     
  15. def consoleAppender = { line -> println line }               
  16.   
  17. // 4) Put it all together  
  18. def myConf = configurator.curry(dateFormatter, debugFilter)     
  19. def myLog  = appender.curry(myConf, consoleAppender)            
  20.   
  21. myLog('here is some debug message')  
  22. myLog('this will not be printed')  
This pattern is extremely flexible, because the logic of how the filtering works, how the formatting is applied, and how the result is written is fully configurable (even at runtime). With the help of closures and their curry method, we achieved a solution with the best possible coherence and lowest possible coupling. Note how each of the closures completely addresses exactly one concern. 

This is the beginning of functional programming. See Andrew Glover’s excellent online article on functional programming with Groovy closures at http://www-128.ibm.com/developerworks/library/j-pg08235/. It expands on how to use this approach for implementing your own expression language, capturing business rules, and checking your code for holding invariants. 

Classification via the isCase method 
Closures implement the isCase method to make closures work as classifiers in grep and switch . In that case, the respective argument is passed into the closure, and calling the closure needs to evaluate to a Groovy Boolean value (see section 6.1). As you see in 
  1. assert [1,2,3].grep{ it<3 } == [1,2]  
  2. switch(10){  
  3.     case {it%2 == 1} : assert false  
  4. }  
this allows us to classify by arbitrary logic. Again, this is only possible because closures are objects

Remaining methods 
For the sake of completeness, it needs to be said that closures support the clone method in the usual Java sense. The asWriteable method returns a clone of the current closure that has an additional writeTo(Writer) method to write the result of a closure call directly into the given Writer

Finally, there are a setter and getter for the so-called delegate. We will cross the topic of what a delegate is and how it is used inside a closure when investigating a closure’s scoping rules in the next section.

沒有留言:

張貼留言

網誌存檔

關於我自己

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