Freitag, 4. Januar 2008

Dieser Blog ist tot. Ich blogge weiter auf dem «Agile Trail».

Kata in Groovy ge-DRY-t

Neulich schrieb mir ein it-agile-Kollege...

Von: Sanitz, Sebastian (Blog) (07.12.2007)
====================================
Hallo Bernd,
zur Zeit tue ich mir doch Groovy und bald auch Grails an. Unter anderem probiere ich mich an den CodeKatas vom pragmatischen David um die Sprache richtig kennen zulernen.

Jetzt habe ich da ein paar Fragen. Meine Lösung zum Wetterproblem ist angehängt, sie funktioniert mittlerweile, ist zum Vergleich mit meinen stümperhaften Anfängen "grooviger" bzw. kleiner gemacht. Das Ergebnis sieht so aus:

Tag: 14 Spread:  2 Min: 59 Max: 61
Tag: 15 Spread: 9 Min: 55 Max: 64
Tag: 13 Spread: 11 Min: 59 Max: 70
Tag: 24 Spread: 13 Min: 77 Max: 90
Tag: 12 Spread: 15 Min: 73 Max: 88
Tag: 2 Spread: 16 Min: 63 Max: 79
Tag: 7 Spread: 16 Min: 57 Max: 73
Tag: 28 Spread: 16 Min: 68 Max: 84
Tag: 4 Spread: 18 Min: 59 Max: 77
Tag: 25 Spread: 18 Min: 72 Max: 90
Tag: 27 Spread: 19 Min: 72 Max: 91
Tag: 6 Spread: 20 Min: 61 Max: 81
Tag: 10 Spread: 20 Min: 64 Max: 84
Tag: 16 Spread: 20 Min: 59 Max: 79
Tag: 19 Spread: 20 Min: 61 Max: 81
Tag: 8 Spread: 21 Min: 54 Max: 75
Tag: 3 Spread: 22 Min: 55 Max: 77
Tag: 23 Spread: 22 Min: 68 Max: 90
Tag: 29 Spread: 22 Min: 66 Max: 88
Tag: 5 Spread: 24 Min: 66 Max: 90
Tag: 17 Spread: 24 Min: 57 Max: 81
Tag: 22 Spread: 26 Min: 64 Max: 90
Tag: 20 Spread: 27 Min: 57 Max: 84
Tag: 21 Spread: 27 Min: 59 Max: 86
Tag: 1 Spread: 29 Min: 59 Max: 88
Tag: 18 Spread: 30 Min: 52 Max: 82
Tag: 11 Spread: 32 Min: 59 Max: 91
Tag: 26 Spread: 33 Min: 64 Max: 97
Tag: 30 Spread: 45 Min: 45 Max: 90
Tag: 9 Spread: 54 Min: 32 Max: 86

Hier meine Anfänger-Fragen:
  • Kann ich mit Groovy noch einfacher ein Integer aus dem String machen, als mit Integer.valueOf? (asType wirft natürlich eine Exception...)
  • Gibt es noch eine einfachere Lösung Exemplare von WeatherData mit den Daten zu erzeugen? Meine die Zeile:
    data {{ new WeatherData(day: day, min: min, max: max)
    (Anmerkung: da gehören statt geschweifte spitze Klammern hin, aber damit kommt Blogspot nicht klar :-( )
  • Benutze im Beispiel String.format() für die Ausgabe - gibt es da was groovigeres?
  • Obwohl ich seit über zehn Jahren mit regulären Ausdrücken (früher nur in Perl) hantiere, dauert es sehr lange bis ich eine funktionierende Lösung finde. Hast Du eine schnelle Herangehensweise? Und vielleicht ein anderes Muster für die Aufgabe?
  • Hast Du für die Aufgabe eine ganz andere Lösung?
Viele Grüße - Sebastian

PS
Glaube das liegt an Groovy, man schreibt sehr viel weniger Code und
textet in Emails mehr als sonst ;-)

Von: Schiffer, Bernd (07.12.2007, 23:09 h)
====================================
Hi Sebastian.

> Hier meine Anfänger-Fragen:
> * Kann ich mit Groovy noch einfacher ein Integer aus dem String machen,
> als mit Integer.valueOf? (asType wirft natürlich eine Exception...)
'2'.toInteger()
> * Gibt es noch eine einfachere Lösung Exemplare von WeatherData mit den
> Daten zu erzeugen? Meine die Zeile:
> data {{ new WeatherData(day: day, min: min, max: max)

Nope, das wäre die einfachste mit einem impliziten Konstruktor. Wenn Du einen expliziten Konstruktor benutzt hättest, dann könntest Du über Positional Parameter einfachere WeatherDatas erzeugen:
def data = [day, min, max] as WeatherData
oder
WeatherData data = [day, min, max]
Du brauchst allerdings für Skripte dieser Kürze gar keine Klassen und Objekte, wenn Du so tolle Dinge hast wie literale Maps - später mehr.

> * Benutze im Beispiel String.format() für die Ausgabe - gibt es da was
> groovigeres?

Jein, komme ich gleich zu.

> * Obwohl ich seit über zehn Jahren mit regulären Ausdrücken (früher nur
> in Perl) hantiere, dauert es sehr lange bis ich eine funktionierende
> Lösung finde. Hast Du eine schnelle Herangehensweise? Und vielleicht ein
> anderes Muster für die Aufgabe?

Mein Muster ist ähnlich wie Deines.
Deins:
~/\s+(\d+)\D+(\d+)\D+(\d+).*/
Meins:
def number = /\W+(\d+)/
/(?m)^${number * 3}.*/
Der Unterschied: Deins ist nicht DRY. Du wiederholst zweimal \D+(\d+). Du kannst statt Deines \d+ und Deines \s+ auch ein \W+ nehmen. Dann sind sogar dreimal \D+(\d+). Groovys Slashed Strings sind GStrings, d.h. Du kannst sie substituieren per $-Schreibweise. So kannst Du die Duplikation entfernen.

Mein (?m)^ am Anfang hat mit der Art zu tun, wie ich die Daten einlese.
Deins:
def pattern = ~/\s+(\d+)\D+(\d+)\D+(\d+).*/
new File("weather_orginal.dat").each{ line ->
matcher = pattern.matcher(line);
if (matcher.matches()) {
def day = matcher.group(1)
int max = Integer.valueOf(matcher.group(2))
int min = Integer.valueOf(matcher.group(3))
...
}}
Meins:
def number = /\W+(\d+)/
new File('weather.dat')
.text
.eachMatch(/(?m)^${number * 3}.*/){
all, tag, max, min ->
...
}
(Anmerkung: Wenn man das nicht im Blog schreibt, dann ist das ein Zweizeiler.) Dein Code kompiliert übrigens nicht mit Groovy-1.5.1 (da gibt's kein file.each mehr). Abgesehen davon: Was wir eigentlich gerne hätten wäre ein file.eachLineMatch auf File, aber da es das nicht gibt, nutze ich halt die lange Version file.text.eachMatch (was um ein Zeichen länger ist ;-) ). Und dann habe ich auch die Möglichkeit, sprechender die Gruppen zu benennen mit all, tag, max, min.

Der Grund für (?m)^: ?m ist der MULTILINE-Mode (siehe JavaDoc zu Pattern). Nichts Neues, wenn man JavaRegExp kennt. Über diesen Mode wird ^ zum Beginn einer Zeile, nicht mehr zum Beginn des Strings.

Reguläre Ausdrücke ertüftel ich mir testgetrieben. Sofern es "technische" Probleme gibt, weiche ich auf die GroovyConsole aus. Mit diesem Snippet bekommst Du ganz brauchbare Aussagen darüber, welche Group was gemacht hat:
def regexp = ...
def text = ...
text.eachMatch(regexp){ println it }
> * Hast Du für die Aufgabe eine ganz andere Lösung?

Nein - drei :-)

Schau Dir diese Version an:
def number = /\W+(\d+)/
def eintraege = []
new File('weather.dat')
.text
.eachMatch(/(?m)^${number * 3}.*/){
all, tag, max, min ->
def spread = max.toInteger() - min.toInteger()
eintraege << [tag, spread, min, max] // #1
}
eintraege
.sort{ it[1] } // #3
.each{
println String.format('Tag:%3s Spread:%3s Min:%3s Max:%3s',
*(it*.toString())) // #2
}
Bei #1 erzeuge ich Einträge als Liste von Listen. In den inneren Listen sind die Werte in der Reihenfolge, wie sie von String.format gebraucht werden. Das komische Konstrukt bei #2 - *(it*.toString())) - ruft erst auf allen Werten toString() auf (ich habe ja spread als int gespeichert) um dann das Ergebnis nochmal zu spreaden (*. ist der Dot-Spread-Operator und macht aus einer Liste [a, b] eine kommaseparierte Wertefolge a, b, die man etwa in anderen Listen oder zwecks Parameterübergabe benutzen kann; hat jetzt überhaupt nichts mit dem Spread aus der Kata zu tun!), damit die Parameter für String.format() erzeugt werden. Ich kann auch schreiben it*.toString().toArray() - ist aber länger.

Diese Lösung hatte ich zuerst gebaut. Und dann fiel mir auf, dass die nicht DRY ist: Wenn Du die Reihenfolge der Spalten ändern willst, also z.B. Spread vor Tag oder so, dann musst Du an zwei völlig unterschiedlichen Stellen schrauben, nämlich an der Liste bei #1 und beim beschreibenden String bei #2. Du mußt sogar noch bei #3 etwas ändern, wenn sich Spread nicht mehr an 2. Stelle befindet. Und Du hast das Padding auf 3 gesetzt - an vier Stellen. Fazit dieser Variante: Sehr fragil, unflexibel, redundant - naja, eben nicht DRY.

Dann bin ich auf das hier gekommen:
def number = /\W+(\d+)/
def eintraege = []
new File('weather.dat')
.text
.eachMatch(/(?m)^${number * 3}.*/){
all, tag, max, min ->
def spread = max.toInteger() - min.toInteger()
eintraege << [tag:tag, spread:spread, min:min, max:max] //#1
}
eintraege
.sort{ it.spread } // #3
.each{
println it.collect{ // #2
def label = format(it.key)
def wert = it.value.toString().padLeft(3)
"$label:$wert "
}.join()
}

def format(label) {
label[0].toUpperCase() + label[1..-1]
}
Bei #1 übergebe ich jetzt keine Liste mehr, sondern eine Map (da wird bereits eine LinkedHashMap erzeugt, damit die Einträge in der entsprechenden Reihenfolge bleiben). Da habe ich zwar immer noch Redundanz zwischen Key und Value, aber dazu später mehr. Durch die Map kann ich bei #3 jetzt explizit auf den Key verweisen. Ändere ich die Reihenfolge bei #1 ist davon #3 nicht betroffen. Zu #2: Das ist jetzt die groovyfizierte Version von String.format(). Statt %3s ist der Ausdruck it.value.toString().padLeft(3) gekommen - finde ich jetzt nicht so schön, wo man doch mit %3s so eine nette DSL hat. Besser finde ich da schon format(label), denn damit kann ich mir das Label aus dem Key der Map erzeugen (und wenn Du alles lowercase haben möchtest, dann reicht auch def label = it.key - aber Du hast ja mit der Formatierung angefangen ;-) ).

DRY ist das Ding aber jetzt: Wenn ich an #1 die Reihenfolge ändere, dann zieht #2 schön mit und nichts geht in die Brüche.

Frage mich, ob DRY und String.format() geht. Geht. Hier:
eintraege
.sort{ it.spread }
.each{ // #2
def format = it.keySet().collect{
format(it) + ':%3s'
}.join(' ')
def values = it.values()*.toString()
println String.format(format, *values)
}
Hier ist jetzt nur #2 zu sehen, der Rest blieb gleich. Dachte, dass wäre simpler, als die Variante zuvor, aber wenn ich mir das jetzt so anschaue, dann "gewinnt" mein zweiter Vorschlag. Beide sind DRY, aber die zweite Variante ist IMHO verständlicher als die erste. Ist jetzt aber nur mein Bauchgefühl.

> PS
> Glaube das liegt an Groovy, man schreibt sehr viel weniger Code und
> textet in Emails mehr als sonst ;-)

Echt? Ist mir gar nicht aufgefallen ;-) Mist, Kaffee alle, muß ins Bett!

Viele Grüße,
Bernd

====================================

Die Codesnippets gibt's hier als Nicht-DRY, DRY, und String.format-DRY.

blog comments powered by Disqus