25 sierpnia 2010

Programowania funkcyjnego z Clojure początki niełatwe (szczególnie mentalnie)

Z programowaniem mam do czynienia od kilkunastu lat i zaczyna mi doskwierać coraz bardziej zauważalne ograniczenie mentalne wynikające właśnie z doświadczenia w programowaniu imperatywnym, począwszy od języka BASIC, później Pascal, C i C++, aby ostatecznie osiąść na Javie. W międzyczasie były jeszcze spotkania z innymi językami (nawet funkcyjnymi i logicznymi!), ale nazwijmy to szkolnym epizodem. I właśnie owe nietrwałe przyjaźnie z tymi innymi językami dają mi się teraz we znaki podczas nauki programowania funkcyjnego z Clojure.

Wciąż nie mogę znaleźć odpowiedzi na jedno pytanie, które nurtuje mnie od samego początku spotkania z programowaniem funkcyjnym w wykonaniu Clojure (i rzadko F#) - gdzie są jego zalety w aplikacjach webowych, czy trochę szerzej korporacyjnych?

Sądziłem, że odpowiedź znajdę w książce Practical Clojure, którą właśnie przeczytałem i jestem w trakcie pisania recenzji, ale niestety "Practical" to (w przypadku tej książki) synonim "Definitive Guide", albo "Complete Reference". Taki tytuł wspaniale odpowiadałby zawartości książki.

Każdego dnia staram się przeczytać choć jeden artykuł na temat Clojure i w ogóle programowania funkcyjnego, ale do tej pory jedyne aplikacje, na które trafiam odpowiadają stronie klienckiej, w której czyta się odpowiedź od serwera (w tym sensie, aplikacji na serwerze aplikacyjnym) i przetwarza odpowiednio. Tutaj pasuje mi Clojure jak ulał - zero stanu, a jedynie przekształcanie odpowiedzi przez aplikowanie serii funkcji. Jakby z definicji, wymarzone miejsce dla programowania funkcyjnego (bo jego zamierzeniem jest programowanie z użyciem funkcji jako obywateli pierwszej kategorii, gdzie składanie funkcji odpowiada składaniu znanym wszystkim z matematyki - zero skutków ubocznych, a jedynie operowanie na danych wejściowych).

Do nauki mam narzędzia napisane w Clojure z ich kodami źródłowymi - clojure, clojure-contrib, leiningen, compojure, ring, a ostatnio enlive. Gdybym miał je opisać terminami javowymi powiedziałbym, że clojure i clojure-contrib to język Java z bibliotekami, leiningen to Apache Maven, compojure to Java Servlet API, ring to kontener servletów, a enlive to szablony HTML.

Na chwilę obecną Clojure przypomina mi swego rodzaju serwer aplikacyjny, gdzie usługami są uproszczenia Clojure w kontekście programowania funkcyjnego, w którym akceptuje się skutki uboczne, czyli zmiana stanu zewnętrznego (w stosunku do funkcji), tj. Software Transactional Memory (STM) oraz leniwe struktury, czyli takie, w których wyliczanie kolejnych elementów odbywa się z opóźnieniem, na żądanie.

Kiedy dodać do tego mojego postrzeganie Clojure, w którym w stosunku do Javy ma się tak, jak facelets do JSP w JSF, czyli inna składnia do tworzenia bajtkodu, to niestety (a może stety) nie pomaga mi to w znalezieniu miejsca dla Clojure, kiedy mogę użyć Javy. Jeśli Clojure i Javę potraktować jako (meta?)język do pisania bajtkodu, to zastosowanie jednego nad drugim (w dowolnej kolejności) pozostaje kwestią gustu. Niektóre problemy same wymuszają myślenie funkcyjne, stąd oprogramowanie ich w Clojure będzie mentalnie łatwiejsze, patrz rekurencyjne wzory matematyczne. Takie myślenie się jednak lekko komplikuje, kiedy dodamy do tego rozwiązania w postaci leniwych struktur i STM. W takiej sytuacji możnaby postawić pytanie o sensowność ich użycia w składni niejavowej. Leniwe struktury i STM są częścią Clojure jako rozwiązania (zbioru API) i można ich używać z poziomu Javy bez znajomości składni samego języka Clojure (!)

Wystarczy tego dumania - głowa mnie zaraz rozboli od tego. Pora na coś praktycznego - chwila programowania funkcyjnego z Clojure. Po lekturze An Introduction to Enlive pomyślałem, aby ujawnić siłę ekspresji języka Clojure w postaci jednolinijkowca z doseq, map i partition. Co robi poniższa konstrukcja, która jest złożeniem wspomnianych, trzech funkcji? Ile pracy wymagałoby od Ciebie stworzenie podobnego rozwiązania w Javie? Odpowiedzi w komentarzach pożądane.

Uwaga, każda linia z poleceniem w Clojure rozpoczyna się od "user=>" i nie jest to jego częścią.
user=> (doseq [linia (map (fn [[h s]] (str h " (" s ")")) (partition 2 [1 2 3 4 5 6 7 8 9 0]))] (println linia))
Chyba tak będzie lepiej, co? Nawiasy mogą być bolesne, bez względu na język - Java, Clojure, itp., ale w końcu od tego mamy wsparcie narzędziowe w IDE?!
user=> (doseq
          [linia (map (fn [[h s]] (str h " (" s ")")) (partition 2 [1 2 3 4 5 6 7 8 9 0]))]
            (println linia))
Dla zainteresowanych użyczam dokumentacji każdej z użytej funkcji.
user=> *clojure-version*
{:major 1, :minor 2, :incremental 0, :qualifier ""}
user=> (doc doseq)
-------------------------
clojure.core/doseq
([seq-exprs & body])
Macro
  Repeatedly executes body (presumably for side-effects) with
  bindings and filtering as provided by "for".  Does not retain
  the head of the sequence. Returns nil.
nil
user=> (doc map)
-------------------------
clojure.core/map
([f coll] [f c1 c2] [f c1 c2 c3] [f c1 c2 c3 & colls])
  Returns a lazy sequence consisting of the result of applying f to the
  set of first items of each coll, followed by applying f to the set
  of second items in each coll, until any one of the colls is
  exhausted.  Any remaining items in other colls are ignored. Function
  f should accept number-of-colls arguments.
nil
user=> (doc partition)
-------------------------
clojure.core/partition
([n coll] [n step coll] [n step pad coll])
  Returns a lazy sequence of lists of n items each, at offsets step
  apart. If step is not supplied, defaults to n, i.e. the partitions
  do not overlap. If a pad collection is supplied, use its elements as
  necessary to complete last partition upto n items. In case there are
  not enough padding elements, return a partition with less than n items.
nil

11 komentarzy:

  1. Jesli szukasz dobrych use cases dla programowania funkcyjnego to pomysl o wszystkim, co po pierwsze moze byc zrealizowane bezstanowo, a po drugie moze wprost wymagac zrownoleglenia.

    Zastanow sie, jak moglbys wykorzystac przewage jezyka funkcyjnego nad proceduralnym przy implementacji:
    - czesc wykonawcza enginu ETL, przede wszystkim transformacje
    - czesc wykonawcza systemow BPMS, czyli "wykonywanie" diagramow (kazdy wezel w grafie takiego diagramu to przeciez niezalezny "transformator/reaktor")
    - rozproszony klaster cache/obliczeniowy, idealny przyklad to Coherence, ktory pozwala uploadowac klasy Java przeksztalcajace obiekty cache w inne na danym nodzie,
    - filtry roznego rodzaju: w kolejkach JMS, w servletach, formatery danych na XML, CSV, XLS, itp.
    - ...

    Serdeczne pozdrowienia,
    Michal

    OdpowiedzUsuń
  2. Zaciekawiłeś mnie Jacku tymi postami o Clojure. Ogólnie LISP jest bardzo ciekawym językiem chyba przyjdzie czas i na mnie aby się zagłębić :) Szczególnym argumentem przemawiającym za tym językiem jest dużo mniejsza złożoność od JVM'owych alternatyw jak Scala

    OdpowiedzUsuń
  3. @me, to wciąż jednak ta sfera aplikacyjna, gdzie mnie jeszcze nie ma i pewnie długo nie będzie. Na razie zwykłe aplikacje webowe i tutaj chciałbym znaleźć miejsce dla Clojure. Czyżby to po prostu nie dla niego? W końcu nie chodzi o to, aby uzasadniać zastosowanie, gdzie nie powinno go być :)

    @Rafał, dla mnie najbardziej porywającą sprawą w całym tym poznawaniu PF z Clojure jest właśnie próba znalezienia dla tego zastosowania. Poznaję wiele nowych konstrukcji, ale bez ich wdrożenia, nici z faktycznego zrozumienia. Często pojawia się porównanie ze Scalą i faktycznie jej składnie opisuje się jako trudniejszą do opanowania. W końcu, jeśli oba produkują bajtkod, to dlaczego chciałbym używać trudniejszego/bardziej złożonego języka?!

    OdpowiedzUsuń
  4. Jacku, ale dlaczego do tak raczej konstrukcyjnie najprostszej czesci systemow, jaka jest interfejs webowy, chcesz dodawac zlozonosc? Czyste JSF + komponenty RichFaces jest odpowiednie w 90% przypadkow, przy wiekszych komplikacjach dorzucasz jeszcze biblioteki Springowe i jest dobrze! :)

    Absolutnie nie sadze, zeby naklad zwiazany z doklejaniem czesci kodu w zupelnie innym jezyku mial sens w aplikacjach webowych, ktore z zalozenia powinny byc prostymi kawalkami kodu.

    Co myslisz?

    OdpowiedzUsuń
  5. Nie wiem ile pracy wymagałoby stworzenie podobnego rozwiązania w Javie. Wiem za to, że w Pythonie jest co całkiem łatwe. Dowód: http://coffeefreecode.wordpress.com/2010/08/26/programowania-funkcyjnego-z-pythonem-spotkania/

    OdpowiedzUsuń
  6. @me Wygląda na to, że właśnie takie jest stosowane podejście - Clojure w bebechach aplikacji, gdzieś w tle (w znaczeniu umiejscowienia, nie znaczenia), a na przodzie cokolwiek - może to być JSF, ale również Grails, czy jak obserwuję coś ala RoR. Bez względu na zastosowaną technologię webową do prezentacji, Clojure siedzi z tyłu, wspiera. Brakuje mi jeszcze zrozumienia, jak wykorzystać moc uproszczeń w zrównoleglaniu zadań w Clojure, o których czytam - atomy, refy (nie mylić z reify), itp. Mam wrażenie, że to nie ta klasa zastosowań dla Clojure. A jak trudno mi zrozumieć, jak można programować bez skutków ubocznych, w języku czystym funkcyjnie - Haskellu. Tam to bym się pewnie w ogóle nie odnalazł.

    @kosqx, przeczytałem z zainteresowaniem. Python powiadasz? Kiedyś miałem z nim spotkanie i wydawał mi się zbyt dynamiczny :) Nie przypadł mi do gustu, mimo że spełnił założenia i zrealizowałem zadanie. A jak wrażenia w pisaniu w Clojure? Widzisz jego zastosowanie z Pythonem, czy są bliźniacze w zastosowaniu i nie ma sensu?

    OdpowiedzUsuń
  7. Nie wiem ile w Javie by to kodu zabrało, na pewno więcej. To co mi się w cloujure nie podoba, to że wersja 'lepsza' jest dla mnie równie czytelna co pierwsza. Dużo kodu ma swoje wady i zalety, ja tam się z Javą lubię, mimo iż brakuje mi wielu rzeczy z innych języków.

    OdpowiedzUsuń
  8. @mgruca, podając przykład kodu w Clojure nie miałem zamiaru stawiać na wyższość Clojure przez pryzmat liczby linii potrzebnych do złożenia aplikacji. Mając IDE można o tym zapomnieć. Dyski są tanie :)

    Co mnie jednak przyciąga do Clojure to równie naturalny sposób wyrażania problemów w postaci aplikacji, co i w programowaniu obiektowym, czy imperatywnym. W końcu oba modele - funkcyjny i imperatywny - są identyczne. Wciąż trudno mi w to uwierzyć, ale empirycznie mogę zapewnić, że tak jest. Skoro oba są tożsame, to pozwólmy sobie na chwilę z PF, aby mieć wybór. W samej Javie mamy wybór, ale poza Javą niewiele pozostaje. To właśnie zmieniam.

    OdpowiedzUsuń
  9. Sam przymierzam się od dłuższego czasu do jakiegoś języka poza javą, myślałem o czymś funkcyjnym właśnie. Z studiów miło wspominam pythona, ale inwazja języków na jvm zmusiła mnie do zastanowienia się nad tym wyborem. Scala jest ciekawa, ale nie jestem pewien czy chciałbym w niej programować, to samo z clojure. Grovvy ma dużo zwolenników i sporą popularność (moje subiektywne odczucie), ale nie pasuje mi że jest dynamicznie typowany. Lubię mieć Stringa tam gdzie się go spodziewam a dwa dodane stringi dają dłuższego stringa a nie wynik matematyczny (jak to w perlu mogło by się zdarzyć ;) ) Osobiście jeszcze poczekam z decyzją jaki język wybrać, ale jako że clojure było moim pierwszym typem, to będę obserwować Twoje boje i podpytywać czasem o różne rzeczy :)

    OdpowiedzUsuń
  10. http://pastebin.com/rVhjNdMK dwie przykładowe implementacje w Scali twojego problemu.

    Clojure jest ciekawym językiem do tzw. otwarcia oczu, ale tak samo jak Lisp raczej nie zdobędzie większej popularności przez składnię, która jest zbyt prosta przez co kod przypomina potok nawiasów bez wyraźnej struktury. Scala natomiast ma starą i lubianą składnię C/Javy/Pythona/itd. Owszem Scala może się wydawać bardziej skomplikowana od Javy, ale to złudzenie, bo tak naprawdę to nie Scala jest skomplikowana ale Java prymitywna (odpowiedź twórcy Scali na zarzuty o skomplikowaniu języka http://lamp.epfl.ch/~odersky/blogs/isscalacomplex.html).

    Wiem, że jakiś czas zajmowałeś się Groovym więc powinieneś wiedzieć, że Groovy prawdopodobnie nie powstał by gdyby jego twórca wcześniej poznał Scale :) (http://www.infoq.com/news/2009/07/scala-replace-java).

    No i Scala ma Lifta, rewelacyjny web framework używany m.in przez Foursquare i Novell Pulse.

    OdpowiedzUsuń
  11. @aptu, w ciekawą stronę wędrują komentarze odnośnie Clojure do tego wpisu. Można było domniemywać, że w końcu kiedyś pojawi się choćby próba porównywania go ze Scalą. Niestety na chwilę obecną muszę posiłkować się innych zdaniem, czy i gdzie Scala miałaby zastosowanie. Nie znam jej w ogóle, ale kiedy czytałem Twoje przykłady składnia była lekko zagmatwana i trochę przeraziła mnie swoją złożonością.

    Mówisz, że nawiasy w Clojure są be? Hmmm, tam są tylko takie nawiasy i zawsze w parze, więc choćby z tego powodu wydaje się być składniowo łatwiejsza w poznaniu składni (aby być uczciwym dodam, że są jeszcze nawiasy kwadratowe do wyrażania tablic). Kiedy dodać do tego, że i Clojure, i Scala produkują bajtkod, to na chwilę obecną pozostanę przy poznawaniu Clojure. Może mnie kiedyś przyciągnie do Scali, ale na chwilę obecną niekoniecznie - nie widzę po prostu wartości dodanej. Clojure zacząłem się uczyć, bo brakowało mi wiedzy z programowania funkcyjnego (PF) i jak rozumiem Scala nie dodaje do Javy nic ponad to (w duuuużym uproszczeniu).

    Marzy mi się rozmowa z osobą, która dotknęła obu języków z jednym wcześniejszym doświadczeniem w Javie, tj. nie były Lisper czy podobnie. Ciekawym, jak wyszłoby porównanie. W końcu to jedynie składnia. A może niekoniecznie i czegoś nie dostrzegam?! Wybacz moją ignorancję.

    OdpowiedzUsuń