28 października 2010

monad w Clojure ciąg dalszy - o ich wewnętrznej reprezentacji

Logo ClojureZaczniemy od podniesienia wersji Clojure i Clojure Contrib w naszym środowisku. Nie ma ku temu żadnego, praktycznego powodu, a jedynie poznawanie nowego przez jego użytkowanie (w ten sposób można nawet nie zorientować się, że znamy nowe, bo po prostu było, to się używało). Pamiętamy o ciągłej nauce, samodoskonaleniu, ku uciesze członków zespołu? :-)

Wersja Clojure 1.3.0-alpha2 dostępna jest na jego stronie domowej (sekcja Downloads).

Podniesienie wersji Clojure nie wystarczy do korzystania z monad (w wersji 1.2).
devmac:~ jacek$ clj
Clojure 1.3.0-alpha2
user=> (use 'clojure.contrib.monads)
CompilerException java.lang.ClassNotFoundException: clojure.set, compiling:(clojure/contrib/accumulators.clj:121)
Konieczne jest pobranie standalone-1.3.0-alpha2.jar (z repo clojure.contrib) i umieszczenie w CLASSPATH dla Clojure REPL. Od tej chwili można cieszyć się dobrodziejstwem inwentarza.

Oczywiście w projektach zalecane jest skorzystanie z narzędzia w stylu Apache Maven, np. dla Clojure byłby nim Leiningen, a później należy postępować zgodnie z clojure-contrib 1.3.0-alpha2 deployed to build.clojure.org.

Po tych zmianach uruchamiamy REPL i wczytujemy przestrzeń c.c.monads, w której definiowane są monady.
devmac:~ jacek$ clj
Clojure 1.3.0-alpha2
user=> (use 'clojure.contrib.monads)
nil
Rozgrzewkę mamy za sobą.

Wracając do tematu przewodniego, tym razem nie będzie, do czego służą monady programistom czy matematykom, ale jak się je definiuje w Clojure i czym są w tym języku (ich wewnętrzną reprezentacją). Mam nieodparte wrażenie, że wiele zostało powiedziane o monadach i ten obszar został już dostatecznie zagospodarowany (zainteresowanym polecam przeczytać artykuły, które mam za sobą nt. monad ze znacznikiem "monads" na delicious.com).

Już wiemy (patrz poprzednie wpisy w kategorii clojure), że matematycznie i praktycznie monada to trójka składająca się z konstruktora typu - obliczenia, z którym związane są dwie, obowiązkowe funkcje - w terminologii Clojure będą to m-bind i m-result.

Poznawanie monad w Clojure opieram w dużej mierze na czytaniu artykułów, ale zauważam postęp w zrozumieniu ich sensu, kiedy poza materiałem literackim, uzupełniam go o przegląd źródeł c.c.monads z ich testami (dostępne w repo Gita - clojure-contrib/modules/monads).

Makro monad

Pierwsza konstrukcja tworzenia monad w Clojure to makro monad, które definiuje monadę jako mapę nazw funkcji i ich implementacji (coś ala klasa w Javie). W monadzie-mapie znajdziemy wskazanie na dwie, obowiązkowe funkcje m-result i m-bind oraz opcjonalne m-zero i m-plus.
user=> (monad
[m-result identity
 m-bind (fn [mv f] (f mv))
])
{:m-plus :clojure.contrib.monads/undefined, :m-zero :clojure.contrib.monads/undefined, :m-bind #<user$eval919$m_bind__920 user$eval919$m_bind__920@45570f5c>, :m-result #<core$identity clojure.core$identity@20773d03>}
W ten sposób zdefiniowaliśmy monadę. Tylko, co można z nią zrobić? Nic. W Javie mogłoby to odpowiadać konstrukcji new PewnaKlasa() bez przypisania jej do zmiennej (nie wliczając skutków ubocznych, co jest możliwe, ale nierekomendowane, np. wykonanie statycznej metody, albo stworzenie wątku). W Clojure nie ma zmiennych (to jest koncept języka imperatywnego), a jedynie symbole (koncept języka funkcyjnego).

Innymi słowy, monada w Clojure jest niczym innym jak mapą składającą się z nazw funkcji w postaci kluczy :m-plus, :m-zero, :m-bind i :m-result z ich implementacją (jeśli podana na wejściu).

Dobrze byłoby przypisać nazwę takiej strukturze, np. za pomocą (def nazwa (monad ...)).
user=> (def moja-monada (monad 
 [m-result identity
  m-bind (fn [mv f] (f mv))
  ]))
#'user/moja-monada
user=> moja-monada
{:m-plus :clojure.contrib.monads/undefined, :m-zero :clojure.contrib.monads/undefined, :m-bind #<user$fn__923$m_bind__924 user$fn__923$m_bind__924@54c707c1>, :m-result #<core$identity clojure.core$identity@20773d03>}
user=> (type moja-monada)
clojure.lang.PersistentArrayMap
c.c.monads dostarcza już takiego makro - defmonad.
user=> (doc defmonad)
-------------------------
clojure.contrib.monads/defmonad
([name doc-string operations] [name operations])
Macro
  Define a named monad by defining the monad operations. The definitions
    are written like bindings to the monad operations m-bind and
    m-result (required) and m-zero and m-plus (optional).
nil
user=> (defmonad moja-monada-2 
 [m-result identity
  m-bind (fn [mv f] (f mv))
  ])
#'user/moja-monada-2
user=> moja-monada-2
{:m-plus :clojure.contrib.monads/undefined, :m-zero :clojure.contrib.monads/undefined, :m-bind #<user$fn__927$m_bind__928 user$fn__927$m_bind__928@67b14530>, :m-result #<core$identity clojure.core$identity@20773d03>}

Makro with-monad

Kolejnym makro w c.c.monads jest with-monad. Dzięki niemu operacja monadyczna zostaje związana z konkretną implementacją w danej monadzie.

Weźmy za przykład monady: maybe-m, sequence-m oraz state-m. Każda z nich musi dostarczać dwie metody - m-bind oraz m-result. Każda z nich musi działać na z góry ustalonej strukturze obliczeniowej. Zobaczmy.
user=> (with-monad maybe-m (m-result 1))
1
user=> (with-monad sequence-m (m-result 1))
(1)
user=> (with-monad state-m (m-result 1))
#<monads$fn__774$m_result_state__775$fn__776 clojure.contrib.monads$fn__774$m_result_state__775$fn__776@65cb048e>
W przypadku funkcji m-result jej wynikiem jest zwrócenie wartości monadycznej dla podanej na wejściu - dla maybe-m będzie to po prostu podana wartość, sequence-m zwróci listę jednoelementową z podaną wartością, a state-m zwróci funkcję, która na wejściu wymaga podania stanu (środowiska, w którym będzie działało ciało funkcji "stanowej") i w wyniku jej wykonania dostaniemy parę - wartość, która została podana na wejściu m-result oraz aktualny stan (nim może być cokolwiek i najczęściej jest kolejna mapa z przypisaniami symbol - wartość, jak zmienne w Javie). Sprawdźmy.
user=> (def f-stanowa (with-monad state-m (m-result 1)))
#'user/f-stanowa
user=> (f-stanowa 5)
[1 5]
user=> (f-stanowa {:a 5 :b 7})
[1 {:a 5, :b 7}]
I teraz najlepsze: gdyby sobie wyobrazić (a może po prostu przypomnieć), że każdy program (w dowolnym języku) wykonuje się na pewnym stanie, który moglibyśmy reprezentować jako mapę - zmienna-wartość, to z monadą state-m możemy wykonać program napisany w języku funkcyjnym bez pojęcia stanu, emulując stan monadą. Tworzymy środowisko - stan początkowy - i każde wykonanie funkcji w jego ramach będzie dotyczyło jedynie tego stanu. Zamiast przekazywać stan z funkcji do funkcji (przez ich szeregowanie) można skorzystać z monady state-m i zjąć sobie ten kłopot z barków, wykonując funkcje tak, jakby ten stan po prostu był. Zdecydowanie upraszcza testowanie i zrównoleglanie obliczeń, bo zakłada się, że funkcje nie mają skutków ubocznych i działają w zamkniętej przestrzeni. To było dla mnie niezwykle odkrywcze w swojej prostocie.

Wystarczy tych pieśni na dzisiaj. Jutro kolejna porcja moich znalezisk monadycznych. Pytania i uwagi mile widziane. Jako podsumowanie można oczekiwać prezentacji na Warszawa JUG. Ach, następne będzie 9. listopada z Cezarym Bartoszukiem, który przedstawi temat "Przegląd języków programowania i ich funkcjonalności".