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