niedziela, 27 lipca 2008

Groovy w praktyce - part 2 - "zapomnij o pętlach" czyli elementy stylu funkcyjnego

Dziś opowieść o funkcjach. Przyjmuje na razie pewne uproszczenie, bo tak naprawdę to nie funkcje ale domknięcia (closure) używam. Jednak wiedza czym domknięcia różnią się od funkcji nie jest na razie potrzebna i można.

Plan jest prosty
  1. Jak używa się pętli w Groovy?
  2. Jak to jest możliwe, że to da się tak zrobić?
Odpalam groovysh. Linijki zaczynające się od groovy> to kod wpisany do interpretera. Tekst zaraz za taką linijką, a przed ===> to wyniki println, wartość po ===> to wartość wyrażenia.

Pewną archaiczną konstrukcją języka odziedziczoną po Javie są pętle ;)
int i = -1;
while(i++ < 3){
System.out.println(i);
}
// lub
for(int i = 0; i < 4; i++){
System.out.println(i);
}

Groovy rzeczywiście akceptuje tak napisany kod.
groovy> for(int i = 0; i < 4; i++){ System.out.println(i); }
0
1
2
3
===> null

Ja jednak preferuje styl funkcyjny.
groovy> 4.times{ it -> println(it) }                      
0
1
2
3
===> null

// A najbardziej lubie

groovy> (0..3).each{ println it }
0
1
2
3
===> null

Mając więc do wyboru
for(int i = 0; i < 4; i++){
System.out.println(i);
}
lub
(0..3).each{ println it }

Co wybierzecie? Gdzie łatwiej stwierdzić ile razy coś się wykona, albo co właściwie się stanie?
Nie przekonani? To może inny przykład
String s = "abcde";
for(int i = 0; i < s.length(); i++){
System.out.println(i);
}

//względem

"abcde".each{ znak -> println znak }

No ale jak to jest właściwie możliwe ?
Wszystkie kolekcje oraz String i zakresy w Groovy mają przydatne funkcje jak each, find, collect, every, any itd. o których więcej w następnych częściach. Na razie wystarczy wiedzieć, że funkcje te pobierają jako parametr zmienną będącą funkcją. Funkcja ta pobiera jako parametr jeden obiekt z kolekcji.

No dobrze, ale żeby przesłać funkcję do innej funkcji najpierw trzeba ją zdefiniować.

W Java można tworzyć funkcje jedynie bezpośrednio w klasach, Groovy tak też potrafi.
// Java
class A{
public void f(Object x){
System.out.println(x);
}
}

// Groovy
groovy> class A{ public void f(Object x){ System.out.println(x);} }
===> true

// W Groovy można też pominąć wszystkie typy, pozbyć się średnika i skrócić System.out... do samego println
class B{
public f(x){
println(x)
}
}

//uruchamiam funkcję
groovy> a = new A()
===> A@1de498
groovy> a.f(1)
1
===> null

W Groovy można definiować funkcję praktycznie w dowolnym miejscu. Możemy to zrobić np. za pomocą def.
groovy> def f(x){ println(x) }
===> true
groovy> f(1)
1
===> null

// tak zdefiniowaną funkcje mogę już wysłać do innej funkcji np.:

groovy> (0..3).each(f)
0
1
2
3
===> 0..3

W językach funkcyjnych często tworzy się funkcje anonimowe, które są przypisywane zmiennym lub parametrom innych funkcji. Przykładowo możemy przypisać zmiennej g funkcję pobierającą Object i wypisującą go na ekran jako:
g = { Object it -> println(it); } // ten zapis możemy uprościć pozbywając się informacji o typie i średnika
g = { it -> println(it) } // nawiasy też nie są potrzebne ponieważ f(x) == f x - podobnie jak w Ocaml
g = { it -> println it }

// ostatecznie mamy:
groovy> g = { it -> println it }
===> groovysh_evaluate$_run_closure1@10e434d
groovy> g(1)
1
===> null

// tak stworzoną funkcję możemy ponownie wysłać do innej funkcji:
groovy> (0..3).each(g)
0
1
2
3
===> 0..3

(0..3).each(g) // możemy ponownie pozbyć się nawiasów
(0..3).each g // właściwie nie ma powodu by definiować osobną zmienną, możemy funkcję zdefiniować natychmiast
(0..3).each { i -> println i} // tutaj nazwaliśmy parametr i,
// co ciekawe parametr o nazwie it jest parametrem domyślnym i nie trzeba go w ogóle specyfikować
// więc można napisać:
(0..3).each { println it }

Ostatnia linijka kodu definiuje anonimową funkcję pobierająca jeden obiekt pod nazwą it i każe wykonać tą funkcję dla każdego obiektu z zakresu od 0 do 3 włącznie.
I tym sposobem zatoczyłem koło.

Na taki styl pozwalają języki, które traktują funkcje jak zwykłe obiekty (First-class function), więc np. JavaScript, Ruby, Python, ActionScript, Scala. Jeśli pozwalają to na pewno z niego korzystają. Jeśli komuś się taki styl nie podoba, to muszę przyznać, że ma pecha. Nawet następny standard C++ (C++0x) będzie najpewniej mieć domknięcia, C# już je ma, a Java ma 3 propozycje domknięć i prawdopodobnie w Java 1.7 domknięcia już będą. Pewnie nie tak ładnie syntaktycznie jak w Groovy, ale będą.
Innymi słowy w 2010 wszystkie główne języki programowania będą obsługiwać domknięcia. I to pełne domknięcia, nie tylko funkcje jako obiekty (a co to tak naprawdę oznacza, to może innym razem).

Brak komentarzy: