程式扎記: [ In Action ] Everyday Groovy - Tips and tricks : Useful snippets

標籤

2014年7月9日 星期三

[ In Action ] Everyday Groovy - Tips and tricks : Useful snippets

Preface:
Here are some code snippets that you may find useful when programming in Groovy. They are aimed at being idiomatic. We will show you a novel use of closures, a neat way to modify text with regular expressions, a useful way of indicating progress in command-line applications, a useful tool to display execution results line by line, and some advanced uses of GStrings.

Shuffling a collection
Suppose you have a collection—a list, for example—and you would like to shufflethe content. For instance, you may have track numbers for your Groovy MP3 player and wish to create a random playlist. The Groovy variant of a solution that is often suggested for scripting languages is:
  1. [12345].sort { Math.random() } // very questionable solution  
This works the following way: when a closure that is passed to the sort method does not take two parameters (in which case it would have been used as aComparator) then sort applies the closure to each element before comparing. Because we return random numbers each comparison has a random outcome.

Although this works, it is neither efficient nor guaranteed to be stable with all sort algorithms, nor does it deliver good results. Programming in Groovy means you have the wealth of Java at your disposal, and thus you can use the shuffle method of java.util.Collections:
  1. def list = [1,2,3,4,5]  
  2. Collections.shuffle(list)  
  3. println list  
This solution is efficient and stable, and it leads leads to an even distribution of the shuffled object; each item has an equal probability of being shuffled to a given index.

Scrambling text with regular expressions 
You may have heard about the experiment where text remains readable even though the words in the text are scrambled, as long as the first and last character don’t change. Look at the following scrambled text. Can you read what it means?
Sarbmlce the inner crharatces of words
laenvig the text sltil reabldae for poeple but
not for cutoermps.

- Listing 13.6 Scrambling the inner character of words
  1. def text = '''  
  2. Scramble the inner characters of words             
  3. leaving the text still readable for people but     
  4. not for computers.                                 
  5. '''                                                
  6. println text.replaceAll(/\B\w+\B/) { inner ->  
  7.     def list = inner.toList()  
  8.     Collections.shuffle(list)  
  9.     list.join ''  
  10. }  
We use a regular expression to find all inner word characters. Then, replaceAll replaces all occurrences with the result of a closure that is fed the corresponding match. The match is converted to a list, shuffled, converted to a string, and returned. The regular expression for finding the inner characters of a word models the first and last character as a non-word-boundary (\B) with one or more word characters (\w+) in between. The ability to use a closure to build the replacement value for a regular expression match is often very useful.

Console progress bar
Suppose you have a time-consuming task that you need to apply to every file in a directory. It would be helpful to get some information about the progress: how much has already been processed, how much is still left to do, and which file is currently being processed. The output should not be longer than a single line on the console, showing updated information on-the-fly.

When started on the directory containing this book’s listings, this line may for example read
::::::::: AthleteDAO.groovy

in between be refreshed to
####::::: Mailman.groovy

and finally be
######### x.groovy

Note: This is all one single displayed line that is updated over time, like a normal progress bar. If you have used the wget command-line tool for fetching web content, you have seen the same kind of display there. The processFiles method in listing 13.7 takes a closure argument called notify . This closure is notified whenever a new files starts being processed. This is equivalent to the Observer pattern.

The processFiles method is called with a closure that updates the progress bar whenever it receives a notification. For simplicity, our processing only consists of sleeping a little, and processing is done for files in the current directory only.
- Listing 13.7 Printing a progress bar on the console
  1. def processFiles(notify) {  
  2.     def names = new File('.').list()  
  3.     names.eachWithIndex { name, i ->  
  4.         notify(i * 10 / names.size(), name)  
  5.         sleep 50     
  6.     }  
  7. }  
  8. processFiles { filled, info ->  
  9.     print '\b' * 61  
  10.     print '#'*filled + ':'*(10-filled) +' '+ info.padRight(50)  
  11. }  
Of course, this snippet could be extended in a number of ways. However, even running this simple version on the console is fun and worthwhile.

Self-commenting single-steps
How about a snippet that reads a codebase and prints it to the console with an indication what each line evaluates to? Example output could look like this:
  1. data = [0,1,2,3]         //-> [0, 1, 2, 3]  
  2. data[1..2]               //-> [1, 2]  
  3. data.collect { it / 2 }  //-> [0, 0.5, 1, 1.5]  
Saving this output back to the original file would mean we have written a piece of code that is able to write comments about itself. Listing 13.8 reveals how to achieve this. We split the code by line, ignore empty lines, print each line, and finally evaluate the line and print the result.
- Listing 13.8 Evaluating and printing line-by-line
  1. def show(code) {  
  2.     for (line in code.split("\n")){  
  3.         if (!line) continue  
  4.         print line.padRight(25) + '//-> '  
  5.         println evaluate(line).inspect()  
  6.     }  
  7. }  
  8. show '''  
  9. data = [0,1,2,3]  
  10. data[1..2]  
  11. data.collect { it / 2 }  
  12. '''  
But wait—didn’t we say that you cannot evaluate Groovy code line-by-line? Yes, and the example works only because data has no declaration, which Groovy takes as a hint to put it into the current binding. Each line is evaluated separately, but the binding is passed onto the GroovyShell that conducts the evaluation. The first line addsdata to the binding; the second line reads data from the binding when getting the 1..2 range from it.

What would happen if the first line read List data = [0,1,2,3] ? At that point, data would be a local variable in the script and so would not be added to the binding. The first line would still evaluate correctly, but the second line will fail because data would not be known in the scope of the GroovyShell that evaluates the second line.

That means that the applicability of our single-step printer is very restricted. However, it makes a good example to sharpen your understanding of scripts being classes rather than sequences of evaluated lines.

Advanced GString usage
In the majority of cases, GStrings are used for simple formatting with the placeholders resolved immediately, as in:
  1. println "Now is ${new Date()}"  
GStrings have a special way in which they resolve any contained placeholders. At the time of the GString creation, they evaluate each placeholder and store a reference to the result of that evaluation within the GString object. At the time of transformation into a java.lang.String , each reference is asked for its string representation in order to construct the fully concatenated result.

In other words: Although the placeholder resolution is eager, writing the references is lazy. The interesting point comes when a placeholder reference refers to an object that changes its string representation over time, especially after the GString was constructed. There are a number of objects that behave like this, such as lists and maps that base their string representation on their current content. Listing 13.9 uses a list to demonstrate this behavior and a typical Groovy object that writes itself lazily: a writable closure.
- Listing 13.9 Writing GString content lazily
  1. def count = 0  
  2. def data  = []  
  3. def counter = { it << count }.asWritable()  
  4. def stanza = "content $counter is $data"  
  5. assert 'content 0 is []'  == stanza  
  6. count++  
  7. data << 1  
  8. assert 'content 1 is [1]' == stanza  
Note how the stanza GString first works on the current values of count and data but changes its string representation when count and data change. This behavior enables GStrings to be used as a lightweight alternative to Groovy’s template engines (see section 9.4).

One word of caution: You need to be extremely careful when using such dynamic GStrings as elements of a HashSet or as keys in a HashMap . In general, you should avoid doing so, because the hash code of the GString will change if its string representation changes. If the hash code changes after the GString has been inserted into a map, the map cannot find the entry again, even if you present it with the exact same GString reference.
- More Example on GString
  1. def data = []  
  2. def gs = "data=$data"  
  3. printf "gs.class=%s\n", gs.class.name  
  4. printf "gs='%s'\n", gs  
  5. data << 1  
  6. printf "gs='%s'\n", gs  
  7.   
  8. def map = [:]  
  9. map[gs]="data"  
  10. printf "map[%s]=%s\n", gs, map[gs]      // value="data"  
  11. data << 2  
  12. printf "map[%s]=%s\n", gs, map[gs]      // value=null  
  13. def key = String.format("data=%s", [1])  
  14. printf "map[%s]=%s\n",key, map[key]     // value="data"  
Supplement:
Regular-Expressions.info - Word Boundaries
The metacharacter \b is an anchor like the caret and the dollar sign. It matches at a position that is called a "word boundary". This match is zero-length...


沒有留言:

張貼留言

網誌存檔

關於我自己

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