16 czerwca 2009

Rozdział 5. o domknięciach z "Programming Groovy"

Rozdział 5. "Using Closures" dedykowany jest jednemu z podstawowych wyróżników Groovy - domknięciom. Jest to "one of the Groovy features you'll use the most" (str. 81). A jeśli dodać do tego Grails, to bez domknięć ani rusz. Właśnie to był mój podstawowy powód, dla którego sięgnąłem po Groovy - język programowania w Grails i domknięcia, o których swego czasu trąbiło się wszędzie w związku z Java 7. W Javie 7 domknięć nie będzie, ale to nie ma zupełnie znaczenia skoro są już w Groovy. Domknięcia mogą przypominać wskaźniki do funkcji znanych z C.

Domknięcie w Groovy jest niczym innym, jak anonimowym blokiem kodu, który może mieć parametry wejściowe, zwracać wartość oraz korzystać ze zmiennych zadeklarowanych w kontekście jego wykonania (nie definicji), tzw. zmienne wolne (ang. free variables). Domknięcie możemy przypisać do zmiennej i traktować jako funkcję (wspomniany wskaźnik funkcji to właśnie referencja). Liczę na intuicję :)
 $ groovysh
Groovy Shell (1.6.3, JVM: 1.6.0_14)
Type 'help' or '\h' for help.
------------------------------------------------------------------------------------------------------
groovy:000> dmkncie = { println "$zmienna" }
===> groovysh_evaluate$_run_closure1@3ca754
groovy:000> dmkncie()
ERROR groovy.lang.MissingPropertyException: No such property: zmienna for class: groovysh_evaluate
at groovysh_evaluate$_run_closure1.doCall (groovysh_evaluate:2)
at groovysh_evaluate$_run_closure1.doCall (groovysh_evaluate)
at groovysh_evaluate.run (groovysh_evaluate:2)
...
groovy:000> zmienna = 1
===> 1
groovy:000> dmkncie()
1
===> null
groovy:000> zmienna = 3
===> 3
groovy:000> dmkncie()
3
===> null
groovy:000>
Dla bardziej dociekliwych, żądnych poznania Groovy od strony formalnej, w dopisku, na pierwszej stronie rozdziału, pojawia się wzmianka o domknięciach w Groovy jako \lambda-wyrażeniach (patrz Rachunek lambda w Wikipedii). Nie miałem wcześniej pojęcia, że "Rachunek lambda jest przydatny do badania algorytmów. Wszystkie algorytmy, które dadzą się zapisać w rachunku lambda, dadzą się zaimplementować na maszynie Turinga i odwrotnie." oraz "Rachunek lambda z typami jest podstawą funkcyjnych języków programowania" (za Wikipedią). Jest to kolejny powód, aby nie tylko czytać książki, ale i je relacjonować z odnośnikami do Wikipedii.

Skoro domknięcia mogą być przypisane jako wartości do zmiennych, to możnaby zapytać, czy domknięcia mogą być parametrami wejściowymi funkcji? Odpowiedź jest twierdząca. W końcu to tylko zmienne, a że mają nietypowe zachowanie, kiedy odniesiemy się do ich wartości z nawiasami, no cóż, takie są :) Domknięcia nie mogą być zdefiniowane bez przypisania ich do zmiennej lub jako parametr wejściowy funkcji.

Jeśli domknięcia są ostatnimi parametrami wejściowymi ich przekazanie do funkcji może przypominać samą definicję funkcji dla programisty javowego (!)
 groovy:000> def wypiszZmiennaIWykonajDomkniecie(zmienna, domkniecie) {
groovy:001> println "zmienna: $zmienna, domkniecie: ${domkniecie()}"
groovy:002> }
===> true
groovy:000> wypiszZmiennaIWykonajDomkniecie(5) { "cialo domkniecia" }
zmienna: 5, domkniecie: cialo domkniecia
===> null
I kolejna ciekawostka z dziedziny teoretycznej informatyki/matematyki - funkcja, która przyjmuje na wejściu inną funkcję lub ją zwraca nazywa się funkcją wyższego rzędu i domknięcie nią jest. Nie pamiętam już, kiedy ten teoretyczny "bełkot" tak mi lekko wchodził do głowy :) Ech, gdyby te wszystkie matematyczne formalizmy wprowadzano tak przyjemnie, jak to ma miejsce na przykładzie Groovy. Może to jest sposób, aby uatrakcyjnić wykłady z teoretycznej informatyki - spróbować połączyć przyjemne (nauka języka, który wykorzystamy do pracy zarobkowej) z pożytecznym (zdobędziemy podstawy teoretyczne, dlaczego i jak języki działają)?! To właśnie lubię w tej książce, jest pisana lekkim piórem z wieloma przykładami oraz teoretycznymi dodatkami. Wszystko to sprawia, że poznawanie nowego języka staje się znacznie łatwiejsze i, co ważniejsze, wciąż pozostaje przyjemnym zajęciem.

Domyślnym parametrem domknięcia jest zmienna it. Możemy zmienić jej nazwę, liczbę parametrów wejściowych lub też pozbawić ich całkowicie. Fajny przykład to ilustrujący znajduje się na stronie 95. lub w pliku UsingClosures/ClosuresParameterTypes.groovy.
 groovy:000> d = { println "$it" }
===> groovysh_evaluate$_run_closure1@18f9b75
groovy:000> d 5
5
===> null
groovy:000> d(5)
5
===> null
groovy:000> d = { zmienna -> println "zmienna: $zmienna" }
===> groovysh_evaluate$_run_closure1@1c4a5ec
groovy:000> d 5
zmienna: 5
===> null
groovy:000> d(5)
zmienna: 5
===> null
groovy:000> d = { -> println "Brak parametrow wejsciowych" }
===> groovysh_evaluate$_run_closure1@9dd6e2
groovy:000> d
===> groovysh_evaluate$_run_closure1@9dd6e2
groovy:000> d()
Brak parametrow wejsciowych
===> null
groovy:000> d 4
ERROR groovy.lang.MissingMethodException: No signature of method: groovysh_evaluate$_run_closure1.call()
is applicable for argument types: (java.lang.Integer) values: [4]
at groovysh_evaluate.run (groovysh_evaluate:2)
Sprawdzenie liczby parametrów wejściowych i ich typów możliwe jest za pomocą atrybutów maximumNumberOfParameters oraz parameterTypes domknięcia.

Parametry wejściowe domknięcia mogą, ale nie muszą, posiadać określenia typu - zadziała inferencja typów.

Autor omawia wzorzec opakowane wykonania metody (ang. execute around method), gdzie wykonanie metod obiektu jest opakowane wykonaniem innych, np. dbających, aby plik, z którego będziemy czytać był najpierw otwarty, a później zamknięty zdejmując tym samym obowiązek dbania o pewien ustalony porządek z programisty (i pozwoli mu się tym samym wyszaleć programowaniem "biznesowym", tj. realizacją funkcjonalności aplikacji zamiast jakimiś niskopoziomowymi konstrukcjami niewidocznymi dla końcowego klienta). Domknięcia powodują, że jest to niezwykle proste i wciąż czytelne. Zamiast:
 def z = new Zasob()
z.otworz()
// skoro mamy otwarty zasób możemy wykonywać na nim różne operacje
z.czytaj()
z.pisz()
// na koniec obowiązkowe zamknięcie zasobu i tym samym zwolnienie zajmowanej przez niego pamięci
z.zamknij()
możemy zdefiniować statyczną metodę w klasie Zasob, której zadaniem będzie przygotować właściwe środowisko wykonania:
 def static stworzWlasciweSrodowisko(domkniecie)
{
def z = new Zasob()
try
{
z.otworz()
domkniecie(z)
}
finally
{
z.zamknij()
}
}
W ten sposób tworzymy właściwy kontekst wykonania metod na zasobie bez konieczności poznawania, jak ten kontekst konstruować.
 Zasob.stworzWlasciweSrodowisko { zasob ->
zasob.czytaj()
zasob.pisz()
}
Zdecydowanie łatwiejsze w użyciu i późniejszym utrzymaniu, co?