Kiedy kolejny raz przesłuchiwałem nagrania z prezentacji Josha Grahama podczas konferencji QCon i jednocześnie próbowałem się z moimi "wymyślnymi" (czytaj: trywialnymi do bólu) przykładami w Eclipse z CounterClockWise (CCW), wszystko stało się jasne. Zgoda. Może nie wszystko, ale więcej i chociażby monadę maybe-m mogę zaliczyć do tych trywialniejszych.
Nie jestem w stanie wyrazić tego błogiego stanu uniesienia, kiedy w końcu wysiłek zrozumienia monad w Clojure nie idzie na marne i po przynajmniej miesiącu zagłębiania się w różnego rodzaju materiały w Sieci i poza nią, maybe-m zaczyna funkcjonować zgodnie z oczekiwaniami. Teraz powinno być mi łatwiej wyjaśnić, cóż magicznego jest w monadach, co powoduje chęć ich zrozumienia u wielu, acz niewielu niestety ma wystarczająco dużo motywacji, aby przebrnąć przez dostępny materiał i przetrawić go. Wierzę, że seria wpisów, które zaplanowałem na ten temat wypełni choć po części tę lukę (i nie przyczynię się jednocześnie do jeszcze większego zaciemnienia tematu, a wręcz przeciwnie).
Jako, że rozpiera mnie, aby zgłębić więcej o monadach (prawdopodobnie będę musiał przeczytać jeszcze raz artykuły, które odznaczyłem jako przeczytane - patrz moje konto na Delicious), teraz przedstawię jedynie zrąb informacji, aby w kolejnych odsłonach przymierzyć się do artykułu o monadach z przykładami. Gdyby ktoś zechciał mi pomóc, proszę o pobranie źródeł z repozytorium monady-artykuł na GitHubie i przesyłanie łatek. Pisanie artykułu w ten sposób, to zrealizowanie nauki git i Clojure, i monad, i pewnie kilku innych rzeczy za jednym zamachem, więc...czekam na aktywny odzew. Niech nawet będzie na poziomie sugestii, bo bez tego materiał będzie tak zrozumiały, jak osoby przedstawiające.
Tyle w ramach rozgrzewki. Pora na wyjawienie "prawdy oczywistej" o monadach w krótkim wprowadzeniu do wprowadzenia do monady maybe-m w Clojure. Przypomnę, że tym razem nie będzie kodu w Javie - on będzie wynikiem pracy kolektywu :) Za to będzie Clojure. Ostrzegałem.
Mała rozgrzewka w postaci kodu w Clojure. Co będzie jego wynikiem?
(ns
#^{:author "Jacek Laskowski"
:doc "Examples with monads"}
monads
(:use [clojure.contrib monads]))
(domonad maybe-m
[m (do (println 1) :m)
n (do (println 2) nil)
o (do (println 3) :o)
p (do (println 4) nil)
r (do (println 5) :r)]
(println m n o p r))Najważniejsza w tym przykładzie jest konstrukcja (domonad maybe-m ...) (linia 7), której zadaniem jest wykonanie serii obliczeń (ang. computations) w "środowisku" monady maybe-m. Jej działanie w skrócie można przedstawić jako - jeśli w jakimkolwiek kroku obliczeń, jedno z nich zwróci nil, kolejne nie będą wykonane, jak i końcowe, podsumowujące wyrażenie - w naszym przypadku (println ...) na linii 13.A zatem jaki będzie wynik?
Przeanalizujmy konstrukcje występujące w ciele (domonad maybe-m ...) zaczynające się nawiasem kwadratowym (symbolizującym tablice w wielu językach programowania, np. Javie) - linie 8-12. Konstrukcja tablicowa [] wymaga parzystej liczby form i lewej stronie przypisuje wartość z prawej. Tym samym staną się stałymi lokalnymi (nie zmiennymi!). I tak kolejno dla każdej pary (przecinek jest opcjonalny i spacja wystarczy jako separator elementów w tablicy). W naszym przypadku, litery od m do r zostaną zainicjowane kolejno typem kluczowym :m, później nil i tak dalej. Dodatkowo dla poprawy zrozumienia, co się dzieje w każdym kroku dodałem wykonanie (println ...), aby wykonanie inicjowania zostawiło jakikolwiek ślad na konsoli (i abym mógł poznać działanie kodu bez uruchamiania debuggera). Jako, że w jednym kroku przypisania wykonujemy dwie funkcje - (println ...) oraz zwrócenie wartości - konieczne było opakowanie ich funkcją (do ...). To kończy wyjaśnianie sekcji inicjującej.
Ostatnia linia 13. z (println m n o p r) odpowiada wypisaniu wartości stałych m, n, o, p, r na ekran. To odpowiada wywołaniu System.out.println w Javie.
A zatem jaki będzie wynik?
Potrzeba więcej? Zgoda.
Konstrukcja (domonad maybe-m ...) jest tak na prawdę makrem w Clojure, co oznacza, że podczas kompilacji zostanie zamieniona na odpowiadające jemu wywołania funkcji. Można się o tym przekonać korzystając z funkcji (clojure.walk/macroexpand-all). Wynikiem jej działania jest "rozwinięcie" makra na odpowiednie wywołania funkcji w Clojure. Nie zapomnijmy o wykluczeniu wykonania formy będącej argumentem dla (macroexpand-all) apostrofem albo funkcją (quote)!
user=> (macroexpand-all '(domonad maybe-m
[m (do (println 1) :m)
n (do (println 2) nil)
o (do (println 3) :o)
p (do (println 4) nil)
r (do (println 5) :r)]
(println m n o p r)))
(let*
[name__518__auto__ maybe-m
m-bind (:m-bind name__518__auto__)
m-result (:m-result name__518__auto__)
m-zero (:m-zero name__518__auto__)
m-plus (:m-plus name__518__auto__)]
(do
(m-bind (do (println 1) :m) (fn* ([m]
(m-bind (do (println 2) nil) (fn* ([n]
(m-bind (do (println 3) :o) (fn* ([o]
(m-bind (do (println 4) nil) (fn* ([p]
(m-bind (do (println 5) :r) (fn* ([r]
(m-result (println m n o p r)))))))))))))))))))Pewnie te ostatnie nawiasy najbardziej intrygujące, co? ;-) Idzie się do nich przyzwyczaić (jak do klepania klamrowych w Javie).Pozostaje zrozumieć, co robią metody m-bind oraz m-result. Pochodzą one z monady, w ramach której działamy, co w naszym przypadku będzie monadą maybe-m. Poniżej jej definicja.
(defmonad maybe-m
"Monad describing computations with possible failures. Failure is
represented by nil, any other value is considered valid. As soon as
a step returns nil, the whole computation will yield nil as well."
[m-zero nil
m-result (fn m-result-maybe [v] v)
m-bind (fn m-bind-maybe [mv f]
(if (nil? mv) nil (f mv)))
m-plus (fn m-plus-maybe [& mvs]
(first (drop-while nil? mvs)))
])Funkcja m-result oczekuje na wejściu pojedynczego parametru i po prostu zwraca go. Innymi słowy, przekazuje wejście na wyjście bez zmian. Tym samym, opakowanie formy przez m-result nie zmienia wyniku formy (bo m-result nic nie zmienia). Jedynym "cudem" jest działanie m-bind, które występuje, aż 5 razy w naszym przykładzie. Odpowiada to liczbie wykonywanych obliczeń. Funkcja m-bind dostaje na wejściu parametr (wartość monadyczną) i sprawdza, czy jest nil. Jeśli tak, zwraca nil. Zwrócenie nil, efektywnie wyłącza wykonanie kolejnych obliczeń, które mogłyby być niezwykle kosztowne - czasowo i/lub finansowo (!)A zatem jaki będzie wynik?
Teraz wszystko powinno być jaśniejsze. W wyniku działania monady maybe-m otrzymamy wynik ostatniego wyrażenia tylko wtedy, gdy każde z obliczeń (może ich być dowolna liczba) *nie* zwróci nil. W przypadku zwrócenia nil, przetwarzanie się zakończy. Stąd możemy zobaczyć na konsoli jedynie 1 i 2 z przypisania m i n, które "dotknie" nil, co zakończy wykonywanie kolejnych instrukcji.
user=> (domonad maybe-m
[m (do (println 1) :m)
n (do (println 2) nil)
o (do (println 3) :o)
p (do (println 4) nil)
r (do (println 5) :r)]
(println m n o p r))
1
2
nilCzy teraz jest już łatwiej zrozumieć, dlaczego warto stosować monady i dlaczego nazywa się je kontenerami, podobnie jak kontener servletów, czy EJB znanych z serwerów JEE? Monady, podobnie jak kontenery JEE, udostępniają środowisko dla naszych bytów - konstrukcji obliczeniowych. Obliczenia w monadach mogą być tak proste jak w naszym przykładzie, w którym sprowadziłem do przypisania z poprzedzającym je wypisaniem na ekran, ale równie dobrze mogą być tak skomplikowane jak pobranie danych z bazy danych, albo ekranu prezentowanego użytkownikowi i tylko w przypadku zwrócenia wartości innej niż nil (albo wybranej przez nas, kiedy stworzymy własną monadę), pozwala się im na kontynuowanie wykonania kolejnych obliczeń - na ich uruchomienie.Do zapamiętania: zamiast serii if'ów, wystarczy jedynie opakować serię obliczeń w monadę maybe-m i oczekiwać zatrzymania przetwarzania, w momencie pojawienia się niedozwolonej wartości - w naszym przypadku nil. Co było do okazania :-)
Brak komentarzy:
Prześlij komentarz