02 września 2010

Dojrzewam funkcyjnie z Clojure, a nawet udzielam pomocy!

To, że ja uczę się myślenia funkcyjnego poznając Clojure każdy czytelnik tego bloga wie, ale że ja pomagam *komuś*, to już niekoniecznie. Wczoraj wszedłem na wyższy stopień wtajemniczenia i nie tylko ja od kogoś, ale też inni ode mnie! Dzięki uprzejmości Marcina Rz. (nazwisko znane redakcji i nie podam, choćby mnie przypalano - obietnica to obietnica) mogłem wykazać się swoją znajomością Clojure (znaczy się - pytanie było wystarczająco trywialne, abym sobie poradził):

Po pobraniu wersji 1.2 Clojure i Clojure-contrib mam problem z używaniem biblioteki repl-utils. Zależało mi przede wszystkim na funkcji "show" z tej biblioteki.

marcin@xxx:/Shared/volumes/home/marcin/Devel/Clojure/clojure-1.2.0$
/usr/lib/jvm/java-6-sun/bin/java -cp clojure-1.2.0.jar:clojure-contrib-1.2.0.jar clojure.main
Clojure 1.2.0
user=> (use 'clojure.contrib.repl-utils)
java.lang.IllegalStateException: source already refers to: #'clojure.repl/source in namespace: user (NO_SOURCE_FILE:0)

Musiałem wyciąć z clojure-contrib-1.2.0/src/main/clojure/clojure/contrib/repl_utils.clj makro "source", funkcję "apropos" i "javadoc", dalej przebudować clojure-contrib, aby doprowadzić to do działania. Dopiero wtedy mogłem używać biblioteki 'clojure.contrib.repl-utils

Czy u Pana też występuje podobne zjawisko? Zastanawia mnie czy to było konieczne czy problem nie leży gdzieś indziej ? Chętnie zasięgnąłbym Pana opinii.


Ten "Pan" od razu wzmógł moje szare (stare?) komórki do większej aktywności i po chwili (!) wyszło ode mnie:

Zamień na (require) i będzie cacy. Dodatkowo możesz skorzystać z :exclude lub :only w (use).

Co w zasadzie kończy temat, ale nie mogłem oprzeć się pokusie, aby nie zapytać:

Przy okazji, napisz, co skłoniło Cię do użycia Clojure? Jest tyle ciekawszych rzeczy... :)

I po chwili miałem już odpowiedź:

Od kilku lat mam do czynienia z Perl i technologiami Webowymi (AJAX, jQuery, ExtJS, XSLT, SQL, memcached, itd.). Pomimo wielu dobrodziejstw Perl nie jest wystarczający przy obecnym postępie technologicznym. Szukałem czegoś bardziej nowoczesnego, potrafiącego w jak najlepszy sposób wykorzystać możliwości sprzętu i oferującego innowacyjne mechanizmy, dzięki którym można pisać solidny kod mając przy tym wiele satysfakcji. Chciałem także dobrze poznać paradygmat funkcyjny. Interesowałem się z początku Erlangiem, Clojure znałem "ze słyszenia". Dopiero Janek z Fablo.pl zainteresował mnie innowacyjnym modelem współbieżności w Clojure. Zacząłem bardziej poznawać Clojure, kupiłem "Programming Clojure" Stuarta Hallowaya i... wciągnęło mnie. STM jest świetne, składnia języka zgrabna i czytelna. Clojure wprowadza wiele nowego jeśli chodzi o programowanie. Przy okazji, przypominam sobie wiele z Javy, z którą nie miałem do czynienia od czasów studiów ;)
Mam nadzieję, że jeszcze będę miał okazję pracować przy jakimś projekcie w Clojure, a może nawet zatrudnić się jako programista Clojure. Byłoby super. Jest jeszcze wiele do poznania w Clojure, ale zapału nie powinno zabraknąć.

Jeśli to możliwe, chętnie chciałbym się z Tobą dzielić swoimi doświadczeniami/problemami w Clojure.


Możnaby powiedzieć, że nawiązała się pomiędzy nami wspólna nić porozumienia, żeby nie powiedzieć przyjaźni (na razie technicznej, ale kto wie, jak się sprawy potoczą). Może brzmieć lekko ironicznie, ale podchodzę do tego z jak największą powagą i uwagą. Przy okazji ciekawa historia, która przypomina mi wiele innych z kręgów lispowych, w tym i wielu "Clojurian". Mam nieodparte wrażenie, że wiele osób, które wcześniej parały się Lispem zapragnęło wejść w świat Javy, w której bibliotek cała masa, a do tego przenośność. Clojure łączy oba światy doskonale, więc z nim można wykorzystać doświadczenie funkcyjne przy tworzeniu nowoczesnych rozwiązań. Paradoks polega na tym, że mogłoby się wydawać, że z nastaniem Javy i całego ruchu wokół niej, wiele języków zeszło do podziemi, a przynajmniej spowolniło swój rozwój, bo uwagę przykuwało now(sz)e i Lisperzy popadli w rynkową niełaskę - mniej zleceń, wszystko obiektowe, itp. Teraz, z Clojure, sprawa ma się znacznie inaczej. Ze wspomnianym Software Transactional Memory (STM) i w połączeniu z zasadami programowania funkcyjnego, w którym kładzie się nacisk na czystość funkcji (wszystko na czym pracują to parametry wejściowe i zero skutków ubocznych aka utrzymywanie stanu zewnętrznego poza funkcją), programowanie współbieżne staje się banalne, a jeśli nie takie dokładnie, to przynajmniej prostsze. Kiedy do tego dorzucimy homoikoniczność Clojure, w którym reprezentacja programu jest jednocześnie podstawową strukturą danych wykorzystywaną w języku (w Clojure będzie to znana wszystkim programistom dowolnego powszechnego języka - lista) i możliwość zmiany struktury programu w trakcie jego działania, owi "straceńcy" stają się liderami rynku! Idealny moment, aby przywołać tytuł pewnej amerykańskiej komedii "Nieoczekiwana zmiana miejsc". Czego wszystkim życzę!

Na pytanie "Czy pomogę?" zawsze odpowiadam, że tak. W tym konkretnym przypadku jestem wręcz zaszczycony móc komuś pomóc, nawet jeśli...sam jeszcze potrzebuję pomocy.

Dla zainteresowanych większym zaangażowaniem szarych komórek, coś z podwórka Clojure. Analizując pewną aplikację napisaną w Clojure trafiłem na użycie funkcji comp, mapcat oraz concat. Wszystkie należą do przestrzeni (pakietu) clojure.core, więc znać je trzeba.
devmac:~ jacek$ clj
Clojure 1.2.0
user=> (doc comp)
-------------------------
clojure.core/comp
([f] [f g] [f g h] [f1 f2 f3 & fs])
  Takes a set of functions and returns a fn that is the composition
  of those fns.  The returned fn takes a variable number of args,
  applies the rightmost of fns to the args, the next
  fn (right-to-left) to the result, etc.
nil
user=> (doc mapcat)
-------------------------
clojure.core/mapcat
([f & colls])
  Returns the result of applying concat to the result of applying map
  to f and colls.  Thus function f should return a collection.
nil
user=> (doc concat)
-------------------------
clojure.core/concat
([] [x] [x y] [x y & zs])
  Returns a lazy seq representing the concatenation of the elements in the supplied colls.
nil
user=> (concat [1 2] (list 3) [4 5])
(1 2 3 4 5)
user=> (mapcat + (concat [1 2] (list 3) [4 5]))
java.lang.IllegalArgumentException: Don't know how to create ISeq from: java.lang.Integer
(user=> (mapcat #(* % 2) (concat [1 2] (list 3) [4 5]))
java.lang.IllegalArgumentException: Don't know how to create ISeq from: java.lang.Integer
(user=> (mapcat #(list (* % 2)) (concat [1 2] (list 3) [4 5]))
Zostawiłem moje pierwsze próby zrozumienia mapcat w połączeniu z próbą definiowania funkcji anonimowych, co może pomóc i Tobie w rozwikłaniu zagadki.

Pytanie: Co będzie wynikiem działania ostatniej, 31. linii rozpoczynającej się od mapcat?

Odpowiedzi, pytania, sugestie mile widziane, najlepiej jako komentarz do tego wpisu. Zapraszam do zabawy!

5 komentarzy:

  1. (2 4 6 8 10)

    prościej: (map #(* % 2) (concat [1 2] (list 3) [4 5]))
    lub: (map (partial * 2) (concat [1 2] (list 3) [4 5]))

    ;; nie sprawdzałem w replu, mogę się mylić.

    OdpowiedzUsuń
  2. w REPL jest dokładnie tak samo. Mnożenie przez 2 zastosowane dla każdego elementu z kolekcji. Obydwie funkcje (map i mapcat) działają w różny sposób - mapcat wykonuje najpierw map, a potem concat. Rezultat jest ten sam. Czy w przypadku tych dwóch funkcji można mówić o pewnej różnicy w wydajności czy jest to tylko kwestia estetyki kodu ?

    OdpowiedzUsuń
  3. Marcin: uniwersalna odpowiedź na pytanie o wydajność brzmi "zmierz." W Fablo używamy YourKit do profilowania kodu Clojurowego; działa zupełnie dobrze.

    Samo map pewnie będzie szybsze, bo nie musi robić concat :)

    OdpowiedzUsuń
  4. Właśnie, czyli więcej przemawia na korzyść funkcji map :)

    OdpowiedzUsuń
  5. Wracając do tematu "mierzenia". Za pomocą samego Clojure łatwo sprawdzić dla analizowanego kodu, że najlepiej wypada użycie samego map, funkcji anonimowych i... makra (') zamiast funkcji (list):
    user=> (dotimes [_ 3] (time (mapcat #(list (* % 2)) (concat [1 2] '(3) [4 5]))))
    "Elapsed time: 0.254045 msecs"
    "Elapsed time: 0.063877 msecs"
    "Elapsed time: 0.062769 msecs"
    nil
    user=> (dotimes [_ 3] (time (mapcat #(list (* % 2)) (concat [1 2] (list 3) [4 5]))))
    "Elapsed time: 0.259906 msecs"
    "Elapsed time: 0.064793 msecs"
    "Elapsed time: 0.064185 msecs"
    nil
    user=> (dotimes [_ 3] (time (map #(* % 2) (concat [1 2] (list 3) [4 5]))))
    "Elapsed time: 0.16197 msecs"
    "Elapsed time: 0.008715 msecs"
    "Elapsed time: 0.008609 msecs"
    nil
    user=> (dotimes [_ 3] (time (map #(* % 2) (concat [1 2] '(3) [4 5]))))
    "Elapsed time: 0.155737 msecs"
    "Elapsed time: 0.007609 msecs"
    "Elapsed time: 0.007154 msecs"
    nil
    user=> (dotimes [_ 3] (time (map (partial * 2) (concat [1 2] (list 3) [4 5]))))
    "Elapsed time: 0.070294 msecs"
    "Elapsed time: 0.009677 msecs"
    "Elapsed time: 0.009397 msecs"
    nil
    user=> (dotimes [_ 3] (time (map (partial * 2) (concat [1 2] '(3) [4 5]))))
    "Elapsed time: 0.064385 msecs"
    "Elapsed time: 0.008744 msecs"
    "Elapsed time: 0.008306 msecs"
    nil

    OdpowiedzUsuń