06 września 2010

Rozpoznawanie Clojure - monady i teoria kategorii

Logo ClojureWłaśnie dotarło do mnie, że ostatnimi czasy nie ma dnia, abym nie ślęczał nad jakimś materiałem związanym z Clojure - czy to czytanie książek (tym razem The Joy of Clojure z Manning), czy lektura dyskusji na grupie użytkowników Clojure, czy też artykuły (które odkładam z komentarzem na delicious i często ślę na twittera), czy w końcu analiza kodów źródłowych Clojure i próbowanie mojej wiedzy w Clojure REPL. Tym samym, całkowicie przygasiłem działalność wokół Javy i Java EE na rzecz Clojure i programowania funkcyjnego w ogólności. Nie oznacza to, że JEE poszło do lamusa, a programowanie obiektowe zarzuciłem całkowicie, tylko tym razem widzę sens dokończenia sprawy do samego końca - kiedy już przyszło mi zebrać się w sobie na poznanie programowania funkcyjnego w wykonaniu Clojure, to niech mam poczucie pełnego (samo)zrealizowania się. Wygląda na to, że cały zgiełk nowości o Java EE, który wydawał mi się nie do ogarnięcia, teraz jakby przycichł i jeśli o zgiełku informacyjnym można wspomnieć, to jego źródłem jest Clojure. Na razie nie żałuję i wydaje się być doskonałym wyborem jako baza wypadowa do kolejnych języków, które zaplanowałem rozpracować - Jython, JavaScript, JRuby i Scala (dokładnie w tej kolejności, bo nie tylko, że wskazuje się ich rodowód funkcyjny, a jeszcze przydają się w pobocznych działalnościach - Jython to główny język do skryptowania czynności administracyjnych przy IBM WebSphere Application Server, a na poznaniu tego zależy mi bardzo, aby coś, co teraz zabiera kilkugodzinny wyjazd, sprowadziło się do "kilkuminutowego" skryptu, który może również posłużyć jako dokumentacja moich administracyjnych poczynań u klienta).

W jakiś przedziwny sposób wzięło mnie na rozpoznawanie monad. Od kilku dni można zauważyć na moim koncie na twitterze (tym samym na LinkedIn i Facebooku) cały strumień o nich. Zbieram się w sobie, aby ostatecznie zebrać je wszystkie w postaci serii wpisów z przykładami, ale na chwilę obecną jedyne czym mogę się pochwalić to cały zestaw artykułów o nich. Z każdym artykułem przychodzą kolejne i zamiast maleć, wciąż mam otwartych kilkanaście zakładek o monadach. Staram się czytać jedynie te, które poruszają ich realizację w Clojure, ale zdarzają się też te z Haskellem w tle i ostatni...z teorią kategorii - A crash "Monads For Programmers" course (!) Przypomniały mi się od razu nużące lata studiowania matematyki na UMK, z których właśnie zajęcia z algebry uniwersalnej były nad wyraz interesujące (nie mogłem niestety potwierdzić tego faktu ocenami w indeksie). Był też rachunek termów i lambda z ówczesnym dziekanem. Tego typu abstrakcja wchodziła mi znacznie łatwiej niż analiza matematyczna z rachunkami różniczkowymi i niezapomnianymi dyfeomorfizmami, czy rachunek prawdopodobieństwa. Było jeszcze kilka innych zajęć, które nie trawiłem, aby teraz po latach zauważać ich potrzebę do zrozumienia konstrukcji programistycznych. Czasami myślę sobie, że studia są zbyt wcześnie i zdecydowanie za teoretyczne (bez praktycznego przełożenia). Na szczęście teoria kategorii była mi wkładana w stosunkowo przyjemny sposób i teraz cieszę się, że programowanie funkcyjne to raczej algebra niż analiza :) Ważniak na MIMUWie okazuje się kolejny raz skarbnicą wiedzy nt. monad i teorii kategorii w ogólności - Teoria kategorii dla informatyków. Po tytule wnioskuję, że nie ominie mnie przyjemność zapoznania się z tym wykładem.

Kiedy czytałem o monadach, to na myśl przychodziły mi najpierw aspekty czy interceptory, aby ostatecznie skończyć na skojarzeniu ich z dekoratorami. Widząc tak opornego (mentalnie) ucznia możnaby postawić pytanie, czy łatwiej zrozumieć programowanie obiektowe po funkcyjnym, czy odwrotnie. U siebie zauważam powolne acz rosnące zrozumienie programowania funkcyjnego, a najlepiej mi idzie, kiedy całkowicie pozbywam się myślenia obiektowo-imperatywnego. Pewnie doświadcza to każdy próbujący zrozumieć Clojure czy Scalę, a śmiem twierdzić, że i JRuby (wspominając jedynie bardziej popularne języki funkcyjne na JVM).

Jakby na dokładkę, na zakończenie lektury rozdziału 2. "Drinking From the Clojure Firehose" ze wspomnianej książki "The Joy of Clojure", w sekcji 2.7.5 trafiłem na makro .. (dwie kropki), które należy do rozwiązań wspierających programowanie w Clojure korzystając z dobrodziejstw inwentarza javowego przez możliwość wywoływania jej klas i metod. Pojawiła się myśl, aby zrealizować swoją pierwszą funkcję reverse, która jest bardziej ćwiczeniem na moją znajomość Clojure niż potrzebą praktyczną, szczególnie, że jej implementacja już istnieje w samym języku.

Zadanie było proste - wypisz ostatni element listy i tak rekurencyjnie w górę, co szybko okazało się nietrywialne, aby ostatecznie wrócić do lekko zmodyfikowanego, początkowego myślenia, że nie było, ale już jest i będzie proste. Nie od dziś wiadomo, że nauka, np. języka programowania, to ciągłe ćwiczenia i pasmo porażek, które z czasem przerzedzane jest coraz dłuższą serią sukcesów, a nieodzownym elementem jest analiza gotowych rozwiązań - w moim przypadku źródła Clojure, a w zasadzie wywołanie funkcji (source).

Niech moja sesja w REPL będzie historią mojej nauki implementacji reverse.
user=> (doc last)
-------------------------
clojure.core/last
([coll])
  Return the last item in coll, in linear time
nil

user=> (doc rest)
-------------------------
clojure.core/rest
([coll])
  Returns a possibly empty seq of the items after the first. Calls seq on its
  argument.
nil

user=> (doc if)
-------------------------
if
Special Form
  Please see http://clojure.org/special_forms#if
nil

; Tak podszedłem do mojej pierwsze wersji reverse

user=> (defn my-reverse [lista]
  (if (empty? (rest lista)) nil)
    (last lista))

; Tutaj, jakby to ująć, moja wiedza się skończyła...niestety
; a miało być coś z recur, stąd ta forma if

user=> (my-reverse '(1 2 3))
3

; upływa kilka minut, a ja nie mam pomysłu na kontynuowanie ćwiczenia, poza jednym...

user=> (source reverse)
(defn reverse
  "Returns a seq of the items in coll in reverse order. Not lazy."
  {:added "1.0"}
  [coll]
    (reduce conj () coll))
nil

; Co?! Tylko jedna linijka? reduce znam i conj również, ale w conj musi być tajemna moc odwracania!

user=> (doc conj)
-------------------------
clojure.core/conj
([coll x] [coll x & xs])
  conj[oin]. Returns a new collection with the xs
    'added'. (conj nil item) returns (item).  The 'addition' may
    happen at different 'places' depending on the concrete type.
nil

; Ach, to dodawanie może być specyficzne - zawsze na początku

user=> (class ())
clojure.lang.PersistentList$EmptyList

; Sprawdźmy działanie conj
 
user=> (conj () 1)
(1)
user=> (conj () 1 2)
(2 1)

; I wszystko (stało się) jasne. Znajomość API zawsze w cenie. Dla zainteresowanych: Zrealizuj conj.
Przy okazji: pisząc "source" pomyliłem się i napisałem "course". Jak widać, mamy jedynie jedną (!) permutację, aby jedno dawało drugie - dosłownie i w przenośni :)

Niech jednak Cię nie zwiedzie myśl, że zapomniałem się w tym poszukiwaniu wzniosłości programowania funkcyjnego. Moje wysiłki zostaną uwieńczone dopiero wtedy, kiedy uda mi się połączyć siłę serwerów aplikacyjnych Java EE z Clojure, a jedynym z możliwych i niezwykle ciekawych rozwiązań jest takie tworzenie aplikacji webowych, aby składanie strony odbywało się w wielu wątkach, równolegle. To jest już możliwe w Javie, ale z Clojure, programowanie wielowątkowe wydaje się być prostsze. Kłania się coś ala map/reduce dla aplikacji webowych. Jakby w gratisie (to słowo ma dla mnie specjalne znaczenie - patrz 4-dniówka na Podlasiu - pływające krowy, grzybobranie, kajaki i przepiękne krajobrazy), Koziołek ze swoim ostatnim wpisem na blogu podsunął mi myśl, aby pożenić Clojure po stronie serwera z GWT, albo (nieznanym mi bliżej, poza nazwą) Vaadin. Taki pomysł był forowany przy Grails, w którym tworzenie usług to trywiał (znaczne zmniejszenie czasu na ich stworzenie), a wizualizacją zajmowałoby się inne, dedykowane do tego zadania, rozwiązanie, np. GWT.

1 komentarz:

  1. Do list i leniwych sekwencji conj dodaje elementy na początku, do wektorów na końcu, do map i zbiorów posortowanych w takim miejscu, żeby wyszła mapa posortowana, a do haszmap i haszowych zbiorów to nie ma znaczenia, bo one nie są koncepcyjnie posortowane.

    Zobacz:

    > (conj (list 1) 2)
    (2 1)
    > (conj [1] 2)
    [1 2]
    > (conj {:a :b, :b :c} [:c :d]) ;; literał mapowy tworzy mapę nieposortowaną
    {:c :d, :a :b, :b :c}
    > (conj #{:a :b :c} :d) ;; literał zbiorowy to HashSet
    #{:a :c :b :d}
    > (conj (seq [1]) 2)
    (2 1)
    > (conj (sorted-map 1 2 10 20) [5 10])
    {1 2, 5 10, 10 20}
    > (conj (sorted-set :a :b :c) :d)
    #{:a :b :c :d}

    OdpowiedzUsuń