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