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.
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.
OdpowiedzUsuń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}