20 czerwca 2009

Z rozdziału 7. o kolekcjach (listach) z "Programming Groovy"

Rozdział 7. "Working with Collections" w "Programming Groovy: Dynamic Productivity for the Java Developer" Venkata Subramaniama omawia korzystanie z tych cech Groovy, które powodują, że praca z kolekcjami (w rozdziale nacisk kładzie się na java.util.List i java.util.Map) staje się jeszcze bardziej przyjemna niż to ma miejsce w Javie.

W Groovy nie ma typów prostych, a ich użycie jest automatycznie zamieniane na ich odpowiednie typy opakowujące, np. char staje się java.lang.Character a int java.lang.Integer. W przypadku kolekcji możemy pokusić się o podobną analogię - użycie czegoś, co może przypominać konstrukcję tablicy sprowadza się do użycia...(jakieś pomysły?)...java.util.ArrayList.
 $ groovysh
Groovy Shell (1.6.3, JVM: 1.6.0_14)
Type 'help' or '\h' for help.
----------------------------------------------
groovy:000> lista = [ 1, 1, 2, 3, 5, 8, 13 ]
===> [1, 1, 2, 3, 5, 8, 13]
groovy:000> lista.class.name
===> java.util.ArrayList
Warto podkreślić deklarację typu tablicowego, który jest de facto listą. Nie korzystamy z konstrukcji przez new z podaniem rozmiaru lub bez, ale z elementami, jak przywyczailiśmy się w Javie. Jakkolwiek pierwsza jest dostępna w Groovy (wciąż przecież możemy pisać w Javie zanim poczujemy klimaty Groovy), to co istotne, konstrukcja z podaniem elementów jest niedozwolona w Groovy ze względu na...istnienie domknięć.
 groovy:000> l2 = new int[5]
===> [I@1270107
groovy:000> l2.class.name
===> [I
groovy:000> l2.size()
===> 5
groovy:000> l2.each { println "$it" }
0
0
0
0
0
===> [I@1270107
groovy:000> l3 = new int[] {1,1,2,3,5}
ERROR org.codehaus.groovy.control.MultipleCompilationErrorsException:
startup failed, groovysh_parse: 1: unexpected token: 1 @ line 1, column 17.
l3 = new int[] {1,1,2,3,5}
^
1 error
at java_lang_Runnable$run.call (Unknown Source)
Właśnie mnie natchnęło na poszukiwanie analogi i tak sobie myślę, że nigdy nie przyszło mi pomyśleć o tablicy jako byciu typem prostym dla listy. W końcu większość list jest właśnie obsługiwana wewnętrznie przez tablice, a cała pozorna (!) prostota z listami wynika z faktu, że chcemy mieć dynamiczną strukturę, która rozszerza się dynamicznie nie dbając o ilość zajmowanej pamięci. Czy ma uzasadnienie stwierdzenie, że robimy to z czystego lenistwa? Komu chciałoby się dbać o rozmiar tablicy (niezwykle efektywne pamięciowo programowanie), kiedy można machnąć ręką na nadmiar miejsca w pamięci zajmowanej przez listę i właśnie ją stosować? Ja robię to z wygody i mam w wielkim poważaniu, że zazwyczaj moja aplikacja zajmuje więcej miejsca w pamięci niż faktycznie potrzebuje. Teraz stałem się bardziej świadomy i będę jeszcze bardziej zestresowany pisząc aplikacje - poza dobrym stylem programowania, o którym trąbi się wokoło, stosowaniu wzorców projektowych, testowaniu, doszła fobia strat pamięci :) Mogłoby się wydawać, że mistrzowie programowania to najbardziej zestresowani ludzie.

"Chodzenie" po liście jest prawie identyczne z przemieszczaniem się po tablicy w Javie. Parafrazując reklamę - owe "prawie" robi różnicę. W Groovy mamy możliwość chodzenia wstecz z indeksami ujemnymi (minus jest wskazaniem, że idziemy od prawej do lewej). Możemy również wypisać elementy z zadanego zakresu (również z minusami). Uwaga na kolejność indeksów!
 groovy:000> lista
===> [1, 1, 2, 3, 5, 8, 13]
groovy:000> lista[-1] == 13
===> true
groovy:000> lista[-3..-1]
===> [5, 8, 13]
groovy:000> lista[-1..-3]
===> [13, 8, 5]
groovy:000> lista[1..3]
===> [1, 2, 3]
groovy:000> lista[3..1]
===> [3, 2, 1]
Kolejnym udogodnieniem w pracy z listami w Groovy jest pobranie podlisty. Należy jednak pamiętać, że zmiana elementu w podliście zmienia listę macierzystą (!) Typem podlisty jest java.util.RandomAccessSubList. Nawet nie wiedziałem, że taka klasa w ogóle istnieje! I kto powiedział, że Groovy to ZUO?! :)
 groovy:000> lista
===> [1, 1, 2, 3, 5, 8, 13]
groovy:000> podlista = lista[0..3]
===> [1, 1, 2, 3]
groovy:000> podlista.class.name
===> java.util.RandomAccessSubList
groovy:000> podlista[0]
===> 1
groovy:000> lista[0]
===> 1
groovy:000> podlista[0] = 5
===> 5
groovy:000> lista[0]
===> 5
Groovy udostępnia metodę each(), która przyjmuje domknięcie, do którego z kolei przekazywany jest element z listy. Jeśli metoda akceptuje pojedynczy parametr wejściowy, to można opuścić nawiasy i wygląda to bardziej groovy. Pamiętamy o tym, co? Domyślnym parametrem wejściowym jest it, ale możemy go nazwać dowolnie. To też pamiętamy, nie?
 groovy:000> lista
===> [5, 1, 2, 3, 5, 8, 13]
groovy:000> lista.each { println "$it" }
5
1
2
3
5
8
13
===> [5, 1, 2, 3, 5, 8, 13]
groovy:000> lista.each { element -> println "$element" }
5
1
2
3
5
8
13
===> [5, 1, 2, 3, 5, 8, 13]
Autor opisuje różnicę między iteratorem wewnętrznym (m.in. w Groovy) a zewnętrznym (m.in. w Javie). Różnica polega na możliwości kontrolowania iteracji i przy zewnętrznym konieczna jest kontrola końca iteracji. W przypadku Groovy mamy do dyspozycji iterator wewnętrzny i nie musimy o nic dbać - po prostu nasze domknięcie zostanie wykonane z każdym elementem spełniającym warunek funkcji wspierającej iterator, np. wspomniana each(). Poza nią mamy reverseEach() (iterowanie wstecz) oraz eachWithIndex() (iterowanie z dodatkowymi licznikami).
 groovy:000> lista
===> [5, 1, 2, 3, 5, 8, 13]
groovy:000> lista.eachWithIndex { element, index -> println " $index: $element" }
0: 5
1: 1
2: 2
3: 3
4: 5
5: 8
6: 13
===> [5, 1, 2, 3, 5, 8, 13]
W ogóle całe iterowanie w Groovy zdaje się być aż nazbyt wyrafinowane (w pozytywnym tego słowa znaczeniu), bo czy kiedykolwiek przyszło nam do głowy, aby iterować po prostu po literach w napisie?
 groovy:000> napis = "Jacek"
===> Jacek
groovy:000> pojedynczeLitery = []
===> []
groovy:000> for (c in napis) {
groovy:001> pojedynczeLitery += c
groovy:002> }
===> null
groovy:000> pojedynczeLitery
===> [J, a, c, e, k]
groovy:000> pojedynczeLitery[1]
===> a
groovy:000> pojedynczeLitery[1].class.name
===> java.lang.String
Więcej w podręczniku użytkownika Groovy w rozdziale Looping. Aż trudno uwierzyć, że w samej dokumentacji Groovy nie korzysta się z operatora dodawania jako "ekscytującej" alternatywy dla metody List.add(). A można przecież i z wykorzystaniem operatora przesunięcia <<.
 groovy:000> napis = "Jacek"
===> Jacek
groovy:000> pojedynczeLitery = []
===> []
groovy:000> for (c in napis) {
groovy:001> pojedynczeLitery << c
groovy:002> }
===> null
groovy:000> pojedynczeLitery
===> [J, a, c, e, k]
Jeśli chcielibyśmy wykonać domknięcie na każdym elemencie kolekcji i otrzymać ponownie kolekcję ze zmodyfikowanymi elementami korzystamy z metody collect().
 groovy:000> napis
===> Jacek
groovy:000> napis.collect { it }
===> [J, a, c, e, k]
groovy:000> napis.collect { it += 1 }
===> [J1, a1, c1, e1, k1]
Możemy zwrócić jedynie elementy z kolekcji, które spełniają zadany warunek z metodą find(). Warunek opisujemy w domknięciu.
 groovy:000> jacek = "Jacek"
===> Jacek
groovy:000> jacek.find { it == 'a' || it == 'e' }
===> a
Zdziwiony/-a wynikiem find()? Jako możliwa odpowiedź niech posłuży kolejny przykład, tyle że teraz na scenę wchodzi findAll().
 groovy:000> jacek = "Jacek"
===> Jacek
groovy:000> jacek.findAll { it == 'a' || it == 'e' }
===> [a, e]
Teraz jasne? find sprawdza kolejne elementy do pierwszego trafienia i kończy działanie, a findAll przechodzi całą kolekcję.

Nic nie stoi na przeszkodzie, aby wykonać kolejną metodę na zwracanej kolekcji. Poprzedni przykład z findAll() połączymy z size(), albo collect() z sum() (przykład z książki).
 groovy:000> jacek.findAll { it == 'a' || it == 'e' }.size()
===> 2
groovy:000> napis = ['Programming', 'In', 'Groovy']
===> [Programming, In, Groovy]
groovy:000> napis.collect { it.size() }.sum()
===> 19
Wyobraźmy sobie takie konstrukcje w Javie. Pewnie nie byłyby bardzo skomplikowane, ale musielibyśmy pisać je sami. Warto? Nie, jeśli mamy je bezpośrednio wspierane przez język. A teraz wydają mi się po prostu niezbędne.

Kolejna niezwykłość w Groovy to metoda inject(). Pierwszym parametrem wejściowym jest wartość początkowa, która będzie przekazywana domknięciu z samym elementem kolekcji. Owa wartość początkowa może być zmieniana, np. zwiększana/zmniejszana i każdorazowo zostanie przekazana domknięciu. Przykład pożądany, nieprawdaż?
 groovy:000> napis = ['Programming', 'In', 'Groovy']
===> [Programming, In, Groovy]
groovy:000> napis.inject(0) { poprzedniaWartosc, element -> poprzedniaWartosc + element }
===> 0ProgrammingInGroovy
groovy:000> napis.inject(0) { poprzedniaWartosc, element -> poprzedniaWartosc + element.size() }
===> 19
Łączenie elementów kolekcji to robota dla join() z pojedynczym parametrem - separatorem (łącznikiem).
 groovy:000> napis
===> [Programming, In, Groovy]
groovy:000> napis.join(' ')
===> Programming In Groovy
groovy:000> napis.join('+')
===> Programming+In+Groovy
Jako przykład wprost stworzonego dla join przykładu jest dynamiczne tworzenie ścieżek na systemie plików lub klas.
Metoda reverse() odwraca kolejność elementów, a odejmowanie jest usuwaniem elementów z kolekcji, ale nie, nie tylko pojedynczego, ale również każdego z elementów listy-odjemnika.
 groovy:000> napis
===> [Programming, In, Groovy]
groovy:000> napis - ['In', 'Element, ktory nie istnieje', 'Groovy']
===> [Programming]
Możemy wykonać wiele operacji z iteratorem (metoda z domknięciem), albo skorzystać z operatorem spread - * (gwiazdka), który wykonuje metodę, na każdym elemencie kolekcji. Przypomina to w działaniu collect i okazuje się, że są rzeczywiście synonimami (patrz Spread Operator (*.)).
 groovy:000> napis.size()
===> 3
groovy:000> napis*.size()
===> [11, 2, 6]
groovy:000> napis.collect { it.size() }
===> [11, 2, 6]
Na zakończenie przykład zastosowania operatora spread do rozszczepiania listy na pojedyncze parametry wejściowe.
 groovy:000> def funkcja(a, b, c, d) {
groovy:001> println "$a $b $c $d"
groovy:002> }
===> true
groovy:000> napis
===> [Programming, In, Groovy]
groovy:000> funkcja(*napis)
ERROR groovy.lang.MissingMethodException: No signature of method: groovysh_evaluate.funkcja() is applicable for
argument types: (java.lang.String, java.lang.String, java.lang.String) values: [Programming, In, Groovy]
at groovysh_evaluate.run (groovysh_evaluate:2)
...
groovy:000> napis = ['Czyz', 'Groovy', 'Nie', 'Zachwyca?']
===> [Czyz, Groovy, Nie, Zachwyca?]
groovy:000> funkcja(*napis)
Czyz Groovy Nie Zachwyca?
===> null
Liczba elementów w liście musi być zgodna z liczbą parametrów wejściowych metody. Czyż Groovy nie zachwyca? A podobno są to funkcjonalności języków funkcyjnych, więc...pora na naukę języka funkcyjnego na JVM. Propozycje? I tylko proszę nie wyjeżdżać ze Scala :) Jakoś bliżej mi do Clojure. Ktoś się już z clojure zaprzyjaźnił?. Można o nim poczytać w Learning Clojure na WikiBooks, a nawet po polsku na clojure.pl. Pewnie niedługo i tu. Chętni wesprzeć mnie literacko? Przykłady użycia clojure (czy innych języków funkcyjnych) w projektach mile widziane.