程式扎記: [ In Action ] The collective Groovy datatypes - Working with ranges

標籤

2014年1月4日 星期六

[ In Action ] The collective Groovy datatypes - Working with ranges

Preface: 
Because collections are so prominent in programming, Groovy alleviates the tedium of using them by directly supporting datatypes of a collective nature: ranges, lists, and maps. In accordance with what you have seen of the simple datatypes, Groovy’s support for collective datatypes encompasses new lightweight means for literal declaration, specialized operators, and numerous GDK enhancements. 

The notation that Groovy uses to set its collective datatypes into action will be new to Java programmers, but as you will see, it is easy to understand and remember. You will pick it up so quickly that you will hardly be able to imagine there was a time when you were new to the concept. 

Despite the new notation possibilities, lists and maps have the exact same semantics as in Java. This situation is slightly different for ranges, because they don’t have a direct equivalent in Java. So let’s start our tour with that topic. 

Working with ranges: 
Think about how often you’ve written a loop like this: 
  1. for (int i=0; i<upperBound; i++){  
  2.    // do something with i  
  3. }  
After careful inspection of the variable, the conditional, and the incrementation, we see that it’s an iteration starting at zero and not reaching the upper bound, assuming there are no side effects on i in the loop body. We have to go through the description of how the code works to find out what it does. 

Next, consider how often you’ve written a conditional such as this: 
  1. if (x >= 0 && x <= upperBound) {  
  2.     // do something with x  
  3. }  
The same thing applies here: We have to inspect how the code works in order to understand what it does. Variable x must be between zero and an upper bound for further processing. It’s easy to overlook that the upper bound is now inclusive. 

Now, we’re not saying that we make mistakes using this syntax on a regular basis. We’re not saying that we can’t get used to (or indeed haven’t gotten used to) the C-style for loop, as countless programmers have over the years. What we’re saying is that it’s harder than it needs to be; and, more important, it’s less expressive than it could be. Can you understand it? Absolutely. Then again, you could understand this chapter if it were written entirely in capital letters—that doesn’t make it a good idea, though. 

Groovy allows you to reveal the meaning of such code pieces by providing the concept of a range. A range has a left bound and a right bound. You can do something foreach element of a range, effectively iterating through it. You can determine whether a candidate element falls inside a range. In other words, a range is an interval plus a strategy for how to move through it. 

Specifying ranges: 
Ranges are specified using the double dot .. range operator between the left and the right bound. This operator has a low precedence, so you often need to enclose the declaration in parentheses. Ranges can also be declared using their respective constructors. 

The ..< range operator specifies a half-exclusive range—that is, the value on the right is not part of the range: 
  1. left..right  
  2. (left..right)   
  3. (left..<right)  
Ranges usually have a lower left bound and a higher right bound. When this is switched, we call it a reverse range. Ranges can also be any combination of the types we’ve described. Listing 4.1 shows these combinations and how ranges can have bounds other than integers, such as dates and strings. Groovy supports ranges at the language level with the special for-in-range loop. 
- Listing 4.1 Range declarations 
  1. assert (0..10).contains(0)               
  2. assert (0..10).contains(5)               
  3. assert (0..10).contains(10)              
  4.                                          
  5. assert (0..10).contains(-1) == false     
  6. assert (0..10).contains(11) == false     
  7. assert (0..<10).contains(9)               
  8. assert (0..<10).contains(10) == false     
  9.                      
  10. // 1) References to ranges                       
  11. def a = 0..10                 
  12. assert a instanceof Range     
  13. assert a.contains(5)          
  14.            
  15. // Explicit construction                                 
  16. a = new IntRange(0,10)     
  17. assert a.contains(5)       
  18.                                                 
  19. assert (0.0..1.0).contains(0.5)        
  20.   
  21. // 2) Date ranges                                 
  22. def today     = new Date()                
  23. def yesterday = today-1                   
  24. assert (yesterday..today).size() == 2     
  25.                        
  26. assert ('a'..'c').contains('b')   // 3) String ranges  
  27.           
  28. // for-in-range loop                                  
  29. def log = ''               
  30. for (element in 5..9){     
  31.     log += element         
  32. }                          
  33. assert log == '56789'      
  34.   
  35. // Loop with reverse range  
  36. log = ''                   
  37. for (element in 9..5){     
  38.     log += element         
  39. }                          
  40. assert log == '98765'      
  41.   
  42. // 4) Half-exclusive, reverse, each with closure  
  43. log = ''                      
  44. (9..<5).each { element ->     
  45.     log += element            
  46. }                             
  47. assert log == '9876'    
Ranges are objects: 
Because every range is an object, you can pass a range around and call its methods. The most prominent methods are each , which executes a specified closure for each element in the range, and contains , which specifies whether a value is within a range or not. 

Being first-class objects, ranges can also participate in the game of operator overriding (see section 3.3) by providing an implementation of the isCase method, with the same meaning as contains. That way, you can use ranges as grep filters and as switch cases. This is shown in listing 4.2. 
- Listing 4.2 Ranges are objects 
  1. // Iterating through ranges  
  2. result = ''                  
  3. (5..9).each{ element ->      
  4.     result += element        
  5. }                            
  6. assert result == '56789'     
  7. assert (0..10).isCase(5)  
  8. age = 36       
  9.   
  10. // 1) Ranges for classification  
  11. def insuranceRate;                                         
  12. switch(age){                                          
  13.     case 16..20 : insuranceRate = 0.05 ; break        
  14.     case 21..50 : insuranceRate = 0.06 ; break        
  15.     case 51..65 : insuranceRate = 0.07 ; break        
  16.     defaultthrow new IllegalArgumentException()     
  17. }                                                     
  18. assert insuranceRate == 0.06  
  19.   
  20. // 2) Filtering with ranges  
  21. ages = [20,36,42,56]  
  22. midage = 21..50  
  23. assert ages.grep(midage) == [36,42]  
Ranges in action: 
Listing 4.1 made use of date and string ranges. In fact, any datatype can be used with ranges, provided that both of the following are true: 
* The type implements next and previous ; that is, it overrides the ++ and -- operators. 
* The type implements java.lang.Comparable ; that is, it implements compareTo , effectively overriding the <=> spaceship operator.

As an example, we implement a class Weekday in listing 4.3 that represents a day of the week. From the perspective of the code that uses our class, a Weekday has a value 'Sun' through 'Sat' . Internally, it’s just an index between 0 and 6 . A little list maps indexes to weekday name abbreviations. 

We implement next and previous to return the respective new Weekday object. compareTo simply compares the indexes. With this preparation, we can construct a range of working days and work our way through it, reporting the work done until we finally reach the well-deserved weekend. Oh, and our boss wants to assess the weekly work report. A final assertion does this on his behalf. 
- Listing 4.3 Custom ranges: weekdays 
  1. class Weekday implements Comparable{  
  2.     static final DAYS = [  
  3.         'Sun''Mon''Tue''Wed''Thu''Fri''Sat'  
  4.     ]  
  5.     private int index = 0  
  6.       
  7.     Weekday(String day){index = DAYS.indexOf(day)}  
  8.   
  9.     Weekday next(){  
  10.         return new Weekday(DAYS[(index+1) % DAYS.size()])  
  11.     }      
  12.        
  13.     Weekday previous(){  
  14.         return new Weekday(DAYS[index-1])     
  15.     }  
  16.       
  17.     @Override  
  18.     int compareTo(Object other){  
  19.         return this.index <=> other.index  
  20.     }  
  21.       
  22.     String toString(){  
  23.         return DAYS[index]  
  24.     }  
  25.       
  26.     static void main(args)  
  27.     {  
  28.         def mon = new Weekday('Mon')  
  29.         def fri = new Weekday('Fri')  
  30.         def worklog = ''  
  31.         for (day in mon..fri){  
  32.             worklog += day.toString() + ' '  
  33.         }  
  34.         assert worklog == 'Mon Tue Wed Thu Fri '  
  35.     }  
  36. }  
Using custom ranges is the next step forward. Look actively through your code for possible applications. Ranges slumber everywhere, and bringing them to life can significantly improve the expressiveness of your code. With a bit of practice, you may find ranges where you never thought possible. This is a sure sign that new language concepts can change your perception of the world. 

Supplement: 
Groovy - Operator Overloading

沒有留言:

張貼留言

網誌存檔