Odkrywam na poważnie Javę 8. Jestem tak zachwycona strumieniami, że aż się muszę podzielić!
Przykład (Java 8)
Zacznę od przykładu. Napisałam ostatnio taki kod:
1
2
3
4
returnmap.entrySet().stream()
.filter(entry->entry.getValue()>0)
.map(entry->entry.getKey())
.toArray(String[]::new);
Co robi ten fragment? Zwraca tablicę zawierającą te klucze z mapy, których wartości są większe od 0.
Dokładniej:
Odwołuje się do obiektu mapy (map).
Pobiera zbiór par klucz-wartość przechowywany w tej mapie (entrySet()).
Zamienia ten zbiór na strumień, albo, poprawniej, odczytuje ten zbiór poprzez strumień (stream()).
Filtruje strumień, pozostawiając w nim (a właściwie w nowym strumieniu) tylko te pary klucz-wartość, w których wartość jest większa od zera. Do filtrowania używana jest lambda, czyli definiowana w miejscu anonimowa funkcja zwracająca wartość typu boolean, przekazana jako parametr (.filter(entry -> entry.getValue() > 0)).
Na wszystkich elementach (typu Map.Entry) pozostałych w strumieniu wykonywana jest funkcja zwracająca klucz (.map(entry -> entry.getKey())).
Na samym końcu wywoływana jest metoda toArray z interfejsu Stream, do której przekazujemy generator (toArray(String[]::new)), czyli funkcję, dzięki której strumień wie, ile pamięci zaalokować. Istnieje bezargumentowa wersja tej metody, tyle że przy jej użyciu otrzymamy tablicę obiektów typu Object.
Przykład (Java 7 i starsze)
Po napisaniu tych paru linijek (ciągle mam problemy z formatowaniem takiego kodu i liczeniem linii) zaczęłam się zastanawiać, jak wyglądałby ten kod w wersji przedstrumieniowej. Musiałoby to być coś takiego:
Strumień przetwarzania / deklaracja oczekiwanego wyniku
Kolejność przetwarzania
Sekwencyjna
Nieokreślona – może zostać poddana optymalizacji
Typy danych
W niektórych miejscach, jak pętla for, trzeba je powtórzyć
Kompilator jest w stanie je wywnioskować, nie trzeba ich powtarzać (np. w definicjach lambd)
Układ kodu
Kod z blokami i wcięciami
Łańcuch wywołań (oczywiście w przypadku bardziej złożonych lambd również trzeba stosować bloki)
Uczciwie dodam, że po stronie minusów stosowania strumieni muszę na razie dopisać debugowanie. W przypadku wystąpienia problemu, odrobinę trudniej jest mi się zorientować, co dokładnie poszło nie tak.
Czy naprawdę nie było innego wolnego słowa?
Strumieni z Javy 8 (z pakietu java.util.stream) nie należy mylić ze strumieniami służącymi do obsługi wejścia i wyjścia (z pakietu java.io). Co za geniusz wymyślił tę nazwę – nie wiem. Być może chodziło i uniknięcie jeszcze gorszego słowa “monada”.
Jeśli chcesz wyszukać w Google informacji na temat “nowych” strumieni, używaj nazwy “Java 8 Streams”.
Co się dzieje w tle?
Strumień nie przechowuje własnych danych – można uznać go za “widok” na dane pochodzące z kolekcji lub innego źródła. Podczas potokowego przetwarzania danych na przykład za pomocą kilku kolejnych operacji map, strumień nie jest modyfikowany, tylko zwracany jest wtedy nowy strumień, oferujący inne dane.
Wniosek
Wygodne. Używać.
Kiedy po raz pierwszy, w ramach prezentacji na Poznań JUG, zobaczyłam strumienie, nie byłam zachwycona. Pomyślałam sobie, że to kolejny wynalazek, który świetnie działa na wymyślonych, zabawkowych przykładach, a który okazuje się bezużyteczny w prawdziwym świecie. Nic bardziej mylnego, w pracy korzystam z nich teraz regularnie.
PS. Sklejanie Stringów
Od lat jeden z najbardziej irytujących braków w bibliotece standardowej Javy to sklejacz łańcuchów znaków. Teraz, jeśli chcesz za pomocą np. średników połączyć słowa przechowywane w kolekcji, nie już musisz pisać specjalnej funkcji ani załączać zewnętrznych bibliotek. Można tak:
Od dwóch miesięcy dość rzadko udaje mi się mieć wolne obie ręce. Przyczyna jest dobrze widoczna na załączonym obrazku 🙂 W grudniu udało mi się jednak ukończyć kurs Introduction to Functional Programming w portalu edX. Poniżej moje notatki i wspomnienia. Przykłady prezentowane w materiałach z kursu są napisane w Haskellu (i rzeczywiście tego języka chciałam się nauczyć), ale autorzy stawiają sobie za cel przedstawienie uniwersalnych zasad programowania funkcyjnego. Wiele zadań domowych można było wykonać w wybranym innym języku funkcyjnym, na przykład w Scali.
Czasem uda nam się trochę popracować przy biurku z podnoszonym blatem!
1. Funkcje można definiować na kilka różnych sposobów.
Żeby nie było, że „programowanie funkcyjne” to tylko szumna nazwa: w Haskellu tę samą funkcję można zdefiniować na kilka sposobów, w zależności od potrzeb i osobistego stylu.
Poniżej kilka różnych definicji tej samej prostej funkcji. Dla zabawy zdefiniujmy funkcję przyjmującą jeden argument. Funkcja zwróci dodatnią wartość logiczną wtedy i tylko wtedy, gdy przekazany jej argument liczbowy będzie poprawną odpowiedzią na wielkie pytanie o życie, wszechświat i całą resztę.
Definicja z użyciem wyrażeń warunkowych:
Haskell
1
isAnswern=ifn==42thenTrueelseFalse
Inne wyrażenia warunkowe (guarded expressions):
1
2
isAnswern|n==42=True
|otherwise=False
Dopasowanie do wzorca:
Haskell
1
2
isAnswer42=True
isAnswer_=False
Najprostsza definicja świata, czyli zwykłe wyliczenie wartości:
Haskell
1
isAnswern=n==42
Zachęcam do potestowania.
2. Lambdy to tylko funkcje bez nazwy.
Programowanie funkcyjne wtargnęło przemocą do wielu popularnych języków, więc nie spodziewam się zaszokować tłumów tą informacją. Ci jednak, którzy jeszcze nie wyszli poza świat obiektów, mogą odetchnąć z ulgą: lambdy to tylko funkcje bez nazwy. Przydatne w różnych okolicznościach, na przykład kiedy funkcja ma być użyta tylko raz i ma bardzo krótką definicję.
W Haskellu lambdy wyglądają tak (backslash, zwany też ukośnikiem wstecznym, ma w zamierzeniu przypominać grecki znak λ):
Haskell
1
\x y->x+y
A tak, dla porównania, wyglądają w Javie:
Java
1
(intx,inty)->{returnx+y;}
Skąd taka nazwa? Inspiracją i matematyczną podstawą języków funkcyjnych jest rachunek lambda.
3. Praca z listami to sama przyjemność.
Na obronie pracy magisterskiej zadano mi pytanie, po czym na pierwszy rzut oka poznać kod napisany w języku Lisp. Właściwa odpowiedź jest dość prosta – po listach!
W Haskellu również listy przetwarza się bardzo przyjemnie. Można wygodnie dopasowywać je do wzorców, dzieląc listy na głowę i ogon, jak w tej poniższej definicji:
Haskell
1
first(head:tail)=head
Użycie:
1
2
3
4
5
6
7
8
>first['a','b','c']
'a'
>first[42]
42
>first[]
Program error:pattern match failure:first[]
Jak widać w ostatnim przykładzie, wzorzec (head:tail) nie zostanie dopasowany do pustej listy.
Można także bardzo wygodnie generować listy, nawet nieskończone, przy użyciu dość intuicyjnej składni tzw. list comprehension. Oto przykład, trudniejszy w słownym opisie niż w implementacji: lista kwadratów tych liczb od 1 do 10, które są podzielne przez 2.
1
2
>[x^2|x<-[1..10],modx2==0]
[4,16,36,64,100]
Ładne, prawda?
4. Brak stanu i inne wymagania.
Nie ma jednej obowiązującej definicji programowania funkcyjnego. Według wielu, jest to wręcz najgorzej zdefiniowany paradygmat programistyczny.
Jednym z najistotniejszych wyznaczników kodu funkcyjnego jest brak stanu – ta sama funkcja wywołana z tymi samymi argumentami powinna zawsze zwracać tę samą wartość, najlepiej bez efektów ubocznych (tj. zmieniania czegokolwiek w tle, np. wartości pól obiektu). W dużej mierze właśnie ta właściwość spowodowała renesans popularności języków funkcyjnych. Dlaczego jest to aż tak istotne?
Programując w ten sposób, niejako „w prezencie” dostajemy kod doskonale poddający się zrównoleglaniu. Osobne wątki nie muszą co chwilę zsynchronizować zmieniającego się, współdzielonego stanu.
Kod składający się z funkcji bez efektów ubocznych jest nieporównywalnie prostszy w zrozumieniu i utrzymaniu.
Wyniki raz wywołanych funkcji można zapamiętać w cache, by przy kolejnym wywołaniu z tymi samymi argumentami, po prostu sięgnąć do już wyznaczonej wartości.
Inne ważne cechy języków funkcyjnych to możliwość definiowania: funkcji wyższego rzędu (funkcji, które zwracają inne funkcje lub przyjmują inne funkcje jako argumenty) oraz, oczywiście, lambd.
5. Upierdliwa obsługa operacji I/O.
Czytanie z wiersza poleceń w Javie nigdy nie należało do przyjemności. Musimy pootwierać piętrowe strumienie, czytać z nich, sprawdzając, czy zostało coś jeszcze do pobrania, a na koniec pamiętać o ich zamknięciu (ok, w Javie 7 pojawił się interfejs AutoCloseable, wystarczy tylko pamiętać o jego użyciu).
Haskell pokazuje nam jednak, w pewnym sensie, że może być jeszcze gorzej. Dlaczego? Ponieważ odczytanie ciągu znaków z wejścia wymaga złamania nagięcia ograniczeń, które stanowią o sile języków funkcyjnych.
Funkcja, za pomocą której będziemy pobierali znaki z wejścia, siłą rzeczy musi zwracać odmienne wyniki dla tego samego argumentu. Co gorsza, musi też w tle zmieniać stan naszego świata (przy kolejnych wywołaniach znaki są konsumowane i inny znak czeka w kolejce na odczytanie).
Dla zachowania czystości kodu, a właściwie w celu oddzielenia kodu czystego od „skażonego”, operacje I/O w Haskellu są opakowywane w akcje. Całość „nieczystego” przetwarzania odbywa się wewnątrz akcji, z której na koniec możemy wyciągnąć czystą wartość za pomocą operatora <-.
Wygląda to stosunkowo niewinnie:
Haskell
1
2
3
4
5
6
7
main=do
line<-getLine
ifnullline
thenreturn()
elsedo
putStrLnline
main
ale warto mieć świadomość, co dzieje się pod spodem. W rzeczywistości dotykamy tu najbardziej chyba przerażającego elementu języka, czyli monad. Kod powyżej czyta linię ze standardowego wejścia i wypisuje ją bez zmin na standardowe wyjście.
6. Brzydkie słowo na „m”.
Monady.
Tysiące tutoriali w Internecie. Długie wpisy (po polsku na przykład ten świetnie przygotowany i napisany tekst Jacka Laskowskiego), godzinne nagrania wideo. Duma lub poczucie wyższości osób, które wreszcie to zrozumiały, więc oficjalnie potrafią programować funkcyjnie.
Offtopic: słowo „monada” kojarzy mi się przede wszystkim ze zbiorem opowiadań „Zamknięty świat”, co dowodzi chyba, że nie była to lektura dla dziecka w podstawówce.
Wytłumaczenie, czym są monady, to temat na osobny, długi wpis – jak ten, który linkuję powyżej. W dużym uproszczeniu (zresztą moje rozumienie tego tematu to nadal właśnie duże uproszczenie): monady to kontenery, które pozwalają na tworzenie łańcuchów operacji, porównywanych do linii produkcyjnych, gdzie każde kolejne stanowisko dodaje coś od siebie. Za pomocą monad można symulować zmiany stanu i dodawać efekty uboczne. Monada obowiązkowo musi definiować dwie operacje, nazywane najczęściej return (włożenie wartości do kontenera) i bind (wyjęcie wartości w celu wstawienia jej do kolejnej funkcji zwracającej monadę; w Haskellu służy do tego operator >>=).
7. Istnieją frameworki webowe dla Haskella. Serio.
Można pisać aplikacje webowe w Haskellu. Istnieją gotowe frameworki. Więcej niż jeden! Osobiście znam kogoś, kto napisał w tym języku sklep internetowy.
8. Wyjście poza imperatywną strefę komfortu otwiera oczy raz na zawsze.
Kurs, przynajmniej na początku, wydał mi się bardzo prosty. Uznałam wstępnie, że Haskell ma zaskakująco niski próg wejścia – bo przecież nie programowałam wcześniej w żadnym języku funkcyjnym. Podczas rozmów z innymi kursantami dotarłam jednak do innej przyczyny: przez kilka lat sporo programowałam w Prologu. Wygląda na to, że kiedy raz podejmie się wysiłek przestawienia się na inny sposób myślenia – w tym wypadku nieimperatywny, nieobiektowy i mniej algorytmiczny – o wiele łatwiej wnika się w kolejne dziwactwa.
9. Programowanie funkcyjne, nie funkcjonalne!
Łatwo wpaść w pułapkę tłumaczenia angielskiej nazwy functional programming jako „programowanie funkcjonalne”. Nawet polska Wikipedia dopuszcza ten termin, a przecież jest on pozbawiony sensu! Functional znaczy „oparty o funkcje” (w sensie matematycznym), po polsku „funkcyjny”. „Funkcjonalny” to „związany z działaniem” lub „dobrze spełniający swoją rolę”. O programowaniu funkcyjnym można co prawda powiedzieć, że jest funkcjonalne… 😉 Jednak w pełni poprawna jest tylko pierwsza nazwa.
10. Można wziąć kasę za kurs online, w którym po wiedzę o najtrudniejszych zagadnieniach odsyła się do zewnętrznych stron
Kurs Haskella był pierwszym kursem online, za który zapłaciłam – i dobrze się stało, ponieważ bez takiej motywacji na pewno odpadłabym po którejś nieprzespanej nocy.
Tym boleśniej odczułam fuszerkę, na jaką pozwoliła sobie ekipa z Uniwersytetu Technicznego w Delft. Wiedza przekazana w ramach kursu nie była wystarczająca do odrobienia zadań domowych w jego drugiej części. Co gorsza, w wykładach pominięto właśnie te bardziej problematyczne zagadnienia, począwszy od tego „czym, u licha, jest ta monada?”.
Opisy niektórych zadań domowych rozpoczynały się niepozornym poleceniem przeczytania treści zawartych na zewnętrznej stronie, a zewnętrzne strony wyglądały jak, nie przymierzając, strony man w Linuksie. Suchar.
Milsza strona programowania
Ta strona korzysta z ciasteczek. Możesz zablokować je w opcjach przeglądarki, jeśli nie wyrażasz na nie zgody. Rozumiem