18 listopada 2009

Makro wplatania -> raz jeszcze i macroexpand

2 komentarzy
To pewnie jedno z ostatnich wpisów dotyczących Clojure przed rozpoczęciem wspomnianego egzaminu technologicznego, w której na niego nie będzie po prostu miejsca (patrz wpis OSGi, EJB, Eclipse, OpenEJB, Groovy i Grails we Wrocławiu, Warszawie i Opolu).

OSGi i OpenEJB na wrocławski DemoCamp mam opanowane, więc pozostaje jedynie przygotować prezentację - kilka slajdów zdecydowanie wystarczy. Będzie dużo kodu źródłowego, więc dobrze byłoby znaleźć sposób, aby jego prezentacja była w stopniu zadowalającym. Tutaj chętnie zaczerpnąłbym jakiś pomysł z doświadczeń innych. Jeśli widziałeś/widziałaś prezentację, w której gro materiału to kod źródłowy, wesprzyj mnie i wskaż te techniki prezentacji, które są istotne i wpłyną na jego odbiór. Może być w komentarzu, albo na skrzynkę jacekXlaskowski.net.pl (uwaga na X!).

Z wtyczką do OpenEJB na warszawską edycję DemoCamp nie będzie czego prezentować inaczej niż interaktywnie, aczkolwiek zastanawiam się, czy zrzuty ekranu jako slajdy nie byłyby ciekawszą alternatywą. Wiadomo wtedy, ile zajmą czasu, a jeśli zostanie go trochę po slajdach, można uruchomić środowisko i przeklikać to tu, to tam.

Najbardziej jednak przerażony jestem 2-dniowym maratonem z Groovy i Grails. Niby możnaby mówić o nich nawet przez cały tydzień przechodząc od podstaw języka Groovy i jego bardziej zaawansowanych możliwości, aby później zająć się Grailsami, w których tematów do omówienia/prezentacji w działaniu co nie miara, a mimo wszystko mam obawy do właściwego przygotowania. To w końcu będzie 2 dni i jakby to nie nazwać będzie miało to znamiona szkolenia/warsztatów. Kiedy się nad tym zastanowię, obawiam się najbardziej zmęczenia słuchaczy, więc dobrze byłoby przygotować coś odświeżającego, co jakiś czas. I tu ponownie zwracam się z prośbą do osób, które zamierzają uczestniczyć w spotkaniu, bądź mają doświadczenie w przygotowaniu tego typu imprez o wskazówki odnośnie dynamiki wprowdzania materiału. Niejednokrotnie brałem udział w szkoleniach jako prowadzący, ale po prostu zgodnie z materiałem, czasem musiał być teoretyczny i nudny. Wolałbym uniknąć takich sytuacji. Sugestie mile widziane.

Wspomniałem, że może to być ostatni wpis o Clojure przez najbliższe 3 tygodnie, bo po prostu nie będzie on tematem najbliższych prezentacji. I tutaj mam pewien sprytny plan, aby powiązać Clojure, a przynajmniej funkcyjne mechanizmy w stylu map, reduce i filter, z Groovy i Grails. Sam Groovy wspiera konstrukcje funkcyjne i czuję, że warto o nich wspomnieć podczas tych 2-dniowych warsztatów, bo nie tylko ja mógłbym się czymś odmiennym niż OO podzielić z uczestnikami, ale skoro jestem na uniwersytecie, to potencjalnie w zamian ja mógłbym dowiedzieć się czegoś ciekawego o programowaniu funkcynym od słuchaczy. W końcu co dwie głowy, to nie jedna. W Groovy mamy konstrukcje funkcyjne, a w Grails nawet wtyczkę do Clojure - Grails Clojure. W ten sposób mógłbym związać Groovy, Grails i Clojure. Być może nie ma to większego sensu poza skomplikowaniem prostoty tandemu Groovy i Grails, ale możliwość integracji istnieje i kto wie, jakie niesie to ze sobą możliwości. Pomysł jest, a wyjdzie w praniu jak pójdzie jego realizacja.

Wracając do Clojure, to dzisiaj poznawałem tajniki programowania w nim przez pryzmat wątku swap two elements in an arbitrary collection na grupie dyskusyjnej użytkowników Clojure. W nim pojawiło się omawiane wcześniej makro wplatania -> ("thread"), więc choćby z tego powodu warto do niego zajrzeć. Pojawił się tam następujący zapis:
 (defn swap [v i j]
(-> v (assoc i (v j)) (assoc j (v i))))
Wiemy już, że jest on równoważny następującemu zapisowi:
 (defn swap [v i j]
(assoc (assoc v i (v j)) j (v i))
Co mnie jednak zaintrygowało w tym wątku to możliwość rozwiązania makro za pomocą macroexpand-1, tak jak ma to miejsce w trakcie kompilacji. Teraz pamiętam, że czytałem o tym w książce Clojure Programming, ale tam było tyle tego, że się zapomniało.
 user=> (doc macroexpand-1)
-------------------------
clojure.core/macroexpand-1
([form])
If form represents a macro form, returns its expansion,
else returns form.
Zatem możemy naocznie przekonać się, czy faktycznie oba w/w zapisy są równoznaczne. W końcu makro jest rozwijane do pewnej formy w Clojure, a za pomocą macroexpand-1 dowiemy się do jakiej.

Zacznijmy od sprawdzenia, czy makro wplatania jest faktycznie makrem w Clojure (a nie tylko tak się nazywa).
 user=> (doc ->)
-------------------------
clojure.core/->
([x] [x form] [x form & more])
Macro
Threads the expr through the forms. Inserts x as the
second item in the first form, making a list of it if it is not a
list already. If there are more forms, inserts the first form as the
second item in second form, etc.
Każde makro jest specjalnie traktowane w Clojure i fakt bycia makrem jest wskazane przez funkcję (doc) przez wypisanie "Macro". To wystarczy. Sprawdźmy, co będzie wynikiem rozwinięcia makra -> w przypadku przykładu wyżej ze swap.
 user=> (macroexpand-1 '(-> v (assoc i (v j)) (assoc j (v i))))
(clojure.core/-> (clojure.core/-> v (assoc i (v j))) (assoc j (v i)))
Warto zauważyć, że aby zadziałało makro musimy przekazać mu listę do rozwinięcia (jak ma to miejsce w fazie wczytywania kodu przez analizatorem składniowym Clojure). Stąd skorzystałem z konstrukcji '(), która tworzy listę będącej postacią formy podczas wczytywania kodu przed kompilacją (faza rozwijania makr).

Jak widać samo macroexpand-1 nie wystarczy, bo rozwija jedynie pierwsze wystąpienie makra. Zdecydowanie za mało, aby zorientować się w jego działaniu. Sądziłem, że potrzebujemy kolejnej funkcji macroexpand, która wywołując macroexpand-1 wielokrotnie rozwinęłaby serię form do postaci "bezmakrowej".
 user=> (doc macroexpand)
-------------------------
clojure.core/macroexpand
([form])
Repeatedly calls macroexpand-1 on form until it no longer
represents a macro form, then returns it. Note neither
macroexpand-1 nor macroexpand expand macros in subforms.

user=> (macroexpand '(-> v (assoc i (v j)) (assoc j (v i))))
(assoc (clojure.core/-> v (assoc i (v j))) j (v i))
Jakież było moje zdziwienie, kiedy przekonałem się, że nie, a przecież nie powinienem się dziwić, bo wyraźnie napisane w dokumentacji macroexpand "neither macroexpand-1 nor macroexpand expand macros in subforms" :( Może jednak się da, a ja o tym nie wiem?! Pomysły?

OSGi, EJB, Eclipse, OpenEJB, Groovy i Grails we Wrocławiu, Warszawie i Opolu

2 komentarzy
Nadchodzące 3 tygodnie staną się moim sprawdzianem technologicznym, w którym pojawię się dwukrotnie na Eclipse DemoCamp we Wrocławiu i Warszawie, aby zakończyć z Groovy i Grails na Uniwersytecie Opolskim.

Najpierw będzie Wrocław. W nadchodzącą sobotę, 21.listopada 2009, podczas 2. edycji Eclipse DemoCamp przedstawię temat "Enterprise JavaBeans (EJB) na OSGi". Agendę zaplanowałem na 1,25h:
  1. Wstęp do OSGi (pakunek, aktywator, MANIFEST.MF, usługi, różne implementacje - Apache Felix, Eclipse Equinox - 15 minut)
  2. Uruchomienie pakunku na platformie OSGi (15 minut)
  3. "OSGi jako platforma SOA (pojedynczej JVM)" - uruchomienie pakunku usługowego (15 minut)
  4. Uruchomienie EJB3 jako pełnoprawnego pakunku OSGi z OSGifikowanym kontenerem EJB3 - Apache OpenEJB 3.1.2 (30 minut)
Będzie to relacja z moich ostatnich doświadczeń z kontenerem EJB3 - Apache OpenEJB i jego migracji na platformę OSGi. Przedstawię problemy, głównie mentalne, z jakimi borykałem się, aby ostatecznie uruchomić paczkę EJB jako pakunek OSGi i wdrożyć ją na OpenEJB, który również działa jako zestaw pakunków OSGi. Podszedłem do tematu około miesiąca temu i jedynym celem było dotarcie do tej konfiguracji wszelkimi sposobami. Udało się i teraz będę mógł to zaprezentować szerszemu gronu.

Póżniej, już w Warszawie, podczas Jesiennego Eclipse DemoCamp 2009 we wtorek, 24. listopada 2009 20 minut z tematem "OpenEJB Eclipse Plugin - EJB 3.1 z Apache OpenEJB w Eclipse IDE". Tym razem przedstawię wtyczkę eclipsową do pracy z EJB 3.1 i Apache OpenEJB. Więcej informacji na temat tego rozwiązania w nagraniu OpenEJB Eclipse Plugin Alpha Released!. Uczestnicy zobaczą wtyczkę na żywo.

Wstęp wolny, aczkolwiek rejestracja na oba Eclipse DemoCamp zalecana (linki są na ich stronach).

Ostatnie 2 dni - 3-4. grudnia 2009 - jestem w Opolu na Uniwersytecie Opolskim, gdzie przedstawię możliwości tandemu Groovy i Grails. Wstępna agenda prezentuje się następująco:

Czwartek 3.12
9:00-10:30 - pierwsza sesja
10:30-10:45 - przerwa kawowa
10:45-12:15 - druga sesja
12:15:13:30 - przerwa obiadowa
13:30-15:00 - trzecia sesja
15:00-15:15 - przerwa kawowa
15:15-16:45 - czwarta sesja

(+ ewentualne spotkanie relaksacyjne po 19:00)

Piątek 4.12
9:00-10:30 - pierwsza sesja
10:30-10:45 - przerwa kawowa
10:45-12:15 - druga sesja
12:15:13:30 - przerwa obiadowa
13:30-15:00 - trzecia sesja
15:00-15:15 - przerwa kawowa
15:15-16:45 - czwarta sesja

i wyjazd do Warszawy. Będzie ciekawie technologicznie. Wszelkie informacje u liderów wspomnianych JUGów. Zapraszam!

p.s. Podczas nauki Clojure na YouTube znalazłem zestaw 10-ciu 10-minutówek o nim (aż mi się język łamię, jak to czytam :)) Tak mi się spodobały prezentacje, że zapożyczyłem sobie ich szablon, który zamierzam wykorzystać podczas moich nadchodzących prezentacji. Zobaczymy, czy uatrakcyjni to odbiór. Dla mnie super!

13 listopada 2009

Funkcyjne wyjście z REPL z (exit) i ankieta językowa

11 komentarzy
Już dawno nie czułem takiej radości z tak niewielkiej ilości kodu. Chyba wciąż jestem przerażony liczbą nawiasów w Clojure, więc kiedy napisałem funkcję (exit) bezbłędnie za pierwszym razem, można sobie wyobrazić, jaki byłem szczęśliwy.
 user=> (def exit (fn [] (. System exit 0)))
#'user/exit
user=> (exit)
Brakowało mi tej funkcji w Clojure REPL, bo ^C, czy ^Z + ENTER pachniały mi jakąś niedoróbką. W tak niewielkiej funkcji udało mi się wykorzystać 3 istotne możliwości Clojure - definiowanie zmiennej przez def, której przypisano funkcję anonimową przez fn, która z kolei wykorzystuje integrację Clojure z Javą za pomocą specjalnej formy . (kropka). Ale ze mnie programista, co? :)

Można to jednak lekko podrasować i wykorzystać specjalną formę #(), która również definiuje funkcję anonimową. Owa funkcja anonimowa w niektórych kręgach nazywana jest...domknięciem (ang. closure). Domknięcie w Clojure nie różni się bardzo w porównaniu do tradycyjnego wywołania funkcji, ale tym razem owe wywołanie fn kończy się stworzeniem nowej. Funkcja, która operuje na innych, w tym zwraca je, nazywamy funkcją wyższego poziomu (ang. higher-order function).

Wracając do naszego #() to możnaby powyższe zapisać tak:
 user=> (def exit #(. System exit 0))
#'user/exit
user=> (exit)
Przeglądając kod źródłowy "Game of Life" Scotta Frasera trafiłem na metodę java.lang.Runtime.availableProcessors(), o której istnieniu nie wiedziałem. W zasadzie o wielu metodach jeszcze nie wiem i wiele z nich wywrze na mnie podobne, pozytywne wrażenie, aczkolwiek ta ma tą zaletę, że utwierdziła mnie w przekonaniu, że mój laptop faktycznie posiada procesor 2-rdzeniowy i 2 wątki powinny działać równolegle (biorąc pod uwagę działanie wszystkich innych programów w tle, pewnie tak nie będzie i jeden z wątków i tak będzie szybszy).
 user=> (def available-procs (.. java.lang.Runtime getRuntime availableProcessors))
#'user/available-procs
user=> (println available-procs)
2
nil
Zadanie dla wytrwałych: Jak zmienić def powyżej, aby możliwe było wykonanie (available-procs), który zwróci 2, tj.
 user=> (available-procs)
2
p.s. Rozpocząłem kolejną ankietę odnośnie języków programowania "Jaki język programowania używasz poza Javą?" (jest na blogu po prawej u góry). Nie ma znaczenia, czy do zabawy, w projekcie, własnej ciekawości, czytasz o nim książkę, czy podobnie. Poświęcasz swój czas, więc wystarczy, aby określić go jako "używasz". Interesuje mnie, czy chociażby rozważasz alternatywę dla Javy i jaki to jest język. Wszystkie języki poza JVM wpadły do kategorii "Inne (poza JVM)". Głosujcie!

12 listopada 2009

56. spotkanie Warszawa JUG - Jesienny Eclipse DemoCamp 2009

0 komentarzy
Warszawska Grupa Użytkowników Technologii Java (Warszawa JUG) zaprasza na 56. spotkanie, które odbędzie się we wtorek, 24. listopada o godzinie 18:00 w sali 5440 Wydziału MIMUW przy ul. Banacha 2 w Warszawie.

Temat: Jesienny Eclipse DemoCamp 2009
Prelegenci: Michał Margiel, Łukasz Lenart, Jacek Laskowski, Sebastian Pietrowski, Tomasz Bujok

Tym razem forma spotkania będzie minikonferencją wokół produktów Eclipse Foundation, w której 5 prelegentów zaprezentuje rozwiązania w ciągu...20 minut każdy. Jeśli to nie przekonuje Cię o spędzeniu wtorkowego wieczoru w niezwykle porywającej atmosferze w gronie entuzjastów javowych, to może agenda?

18:00-18:20 Bądź leniwy - wykorzystaj ukrytą moc Eclipse, czyli jak robić by się nie narobić i wycisnąć max ze środowiska Michał Margiel
18:25-18:45 GoogleAppEngine - chmura na Ja(v)wie Łukasz Lenart
18:50-19:10 OpenEJB Eclipse Plugin - EJB 3.1 z Apache OpenEJB w Eclipse IDE Jacek Laskowski
19:15-19:35 Wprowadzenie do Cloud Foundry Sebastian Pietrowski
19:40-20:00 Po prostu monitoring, czyli jak zbudować skalowalną architekturę monitoringu w 20 minut za pomocą Spring, AOP i ActiveMQ Tomasz Bujok

Harmonogram niezwykle napięty, więc o nudzie nie może być mowy. W miłej atmosferze wiedza sama wchodzi do głowy, mimo późnej godziny.

Wstęp wolny, ale rejestracja wskazana.

Zapraszam w imieniu prelegentów i grupy Warszawa JUG!

11 listopada 2009

(-> Clojure zrozumieć-makro-thread przeczytać-artykuł opisać-na-blogu)

1 komentarzy
Zgodnie z sugestią Michaela zabrałem się za poznawanie Clojure od strony jego kodu źródłowego i podążając słowami mojego mistrza zabrałem się za makro ->, zwane również w kręgach Clojure jako makro "thread".

Kod źródłowy jest niezwykle mały i znajduje się w repozytorium Git - core.clj.
 (defmacro ->
"Threads the expr through the forms. Inserts x as the
second item in the first form, making a list of it if it is not a
list already. If there are more forms, inserts the first form as the
second item in second form, etc."
([x form] (if (seq? form)
`(~(first form) ~x ~@(next form))
(list form x)))
([x form & more] `(-> (-> ~x ~form) ~@more)))
Forma doc zwraca Clojure doc, czyli dokumentację funkcji będącej jej argumentem wejściowym, więc i bez kodu źródłowego możnaby się doczytać, co autor miał na myśli tworząc makro -> (mi to nie wystarczyło, ale pomyślałem, że wspomnę i o tej możliwości).
 user=> (doc ->)
-------------------------
clojure.core/->
([x form] [x form & more])
Macro
Threads the expr through the forms. Inserts x as the
second item in the first form, making a list of it if it is not a
list already. If there are more forms, inserts the first form as the
second item in second form, etc.
nil
Czytając dokumentację dosłownie, makro -> ma za zadanie wpleść wyrażenie do listy form. Wciąż mi to jednak niewiele mówi. Definicja -> składa się z dwóch przypadków - kiedy makro dostaje na wejściu dwa parametry i więcej. Forma (ang. form) to po prostu dwa nawiasy, w których są wywołania funkcji czy makr, lub ponownie form. I tak rekurencyjnie mamy całą masę par nawiasów. W Clojure *wszystko* jest w nawiasach, albo zastąpione pewnymi specjalnymi konstrukcjami - uproszczeniami rozpoznawanymi przez kompilator, które wychodzą poza ten schemat, np. przywoływana już konstrukcja tworzenia kluczy :nazwa jest równoznaczna z wywołaniem funkcji (keyword "nazwa") (często nazywanymi po angielsku "syntactic sugar").

Wykonanie -> z jednym parametrem, to po prostu jego zwrócenie.
 user=> (-> 2)
2
To było proste! :) Teraz będzie znacznie trudniej. Jeśli na wejściu -> mamy dwa parametry, to rozpatrujemy przypadki - 1) forma jest sekwencją (seq? zwraca prawdę) i 2) kiedy nie jest. Przy okazji, nazwy funkcji, których wynikiem jest wartość logiczna true/false są zakończone ? (znak zapytania).

Weźmy ponownie coś równie prostego jak poprzednio.
 user=> ; w nawiasie mamy funkcję minus i jej jedyny argument 3
user=> (-> 2 (- 3))
-1
Dlaczego? Analizując kod źródłowy -> dochodzimy do wniosku, że jest to równoznaczne z poniższym.
 user=> (- 2 3)
-1
Pierwszy argument dla -> jest na drugiej pozycji w pierwszej formie, którą było (- 3). Można jeszcze sprawdzić, jaki był wynik if'a w makro ->.
 user=> (seq? (- 3))
false
Zatem wyliczenie (-> 2 (- 3)) sprowadza się do konstrukcji (list form x), tj. (list - 3 2), a to daje:
 user=> (list - 3 2)
(#<core$___4506 clojure.core$___4506@16de49c> 3 2)
Sprawdźmy, co da wywołanie tego funkcją eval.
 user=> (eval (list - 3 2))
1
Źle! Chyba popełniłem jakiś błąd w rozumowaniu, jaki argument jest przekazywany do seq?, bo w końcu zgodnie z definicją formy, zapis (- 3) nią jest! Biorąc więc pod uwagę moje niedoświadczenie i możliwe pomyłki w rozumowaniu, wchodzimy w sekcję if'a, bo wtedy wszystko jest cacy - 2 staje się drugim argumentem dla odejmowania, a 3 trzecim. Owe szlaczki w makro to sposób na tworzenie kodu w fazie wczytywania programu w Clojure (zaraz przez kompilacją i uruchomieniem), w której następuje rozwiązywanie makr, których celem jest uproszczenie życia programistom przez generowanie bardziej złożonego kodu (po co klepać kilkakrotnie to samo w wielu liniach, jeśli możemy zdefiniować makro raz i wywoływać wielokrotnie w jednej?! "Cudo" znane programistom C/C++).

Wciąż jednak nie rozumiałem, co jest takiego specjalnego w tym makro, aż trafiłem na LexParse, gdzie padła wzmianka o potoku (ang. pipeline). Teraz było znacznie łatwiej zrozumieć, co autor miał na myśli. Aczkolwiek do pełnego zrozumienia daleko (pewnie za mało wciąż napisanych programów w Clojure).
 user=> (-> 2 {2 "dwa"} {"dwa" "DWA"})
"DWA"
Spróbuj samodzielnie znaleźć odpowiedź dlaczego w wyniku otrzymaliśmy "DWA", albo poniżej true:
 user=> (-> '(0 0 0) (count) (odd?))
true
Doświadczenia z Clojure skończyłem artykułem Clojure - Functional Programming for the JVM, którego autorem jest R. Mark Volkmann. Tam znalazłem wzmiankę o Simon Peyton-Jones, który włożył wiele wysiłku w Haskella (czysty język funkcyjny) i miał powiedzieć w OSCON 2007 - Simon Peyton-Jones - A Taste of Haskell Part I:

"In the end, any program must manipulate state. A program that has no side effects whatsoever is a kind of black box. All you can tell is that the box gets hotter."

i dalej już od autora artykułu: The key is to limit side effects, clearly identify them, and avoid scattering them throughout the code.

Warto również wspomnieć o leniwym rozwiązywaniu/wyliczaniu sekwencji - leniwych sekwencjach (ang. lazy sequences) (a może trafniej będzie nazywać je opóźnionymi czy nawet ospałymi czy w końcu uśpionymi sekwencjami?). Elementy w sekwencji nie są wyliczane, aż do momentu ich użycia, co pozwala na tworzenie nieskończonych ciągów danych bez narażania się na wysycenie sterty pamięci JVM. Ta cecha Clojure uzmysłowiła mi o możliwym podejściu w Javie, w której zwykle (zawsze?) wyliczamy wszystkie elementy, nawet jeśli ich nie potrzebujemy. Weźmy za przykład tablicę 150-elementową, albo jeszcze dłuższą. W Javie, w zależności od typu elementów, wielkość potrzebna na ich przechowanie musi być dostępna w trakcie deklaracji. Alternatywą mogłoby być stworzenie takiej kolekcji, w której podajemy liczbę elementów i tyle, a kiedy potrzebny będzie piąty, dziesiąty, czy którykolwiek inny element, wyliczamy go dokładnie w momencie zapytania o niego. Nie sądzę, abym kiedykolwiek myślał o programowaniu w Javie w ten sposób (może dlatego, że po prostu nie miałem okazji?!). Na szczęście nie musimy już tego implementować w Javie, bo mamy Clojure, więc chociażby dla tego warto zwrócić się ku niemu.

Bardzo przypadł mi do gustu następujący akapit:

Is Clojure code hard to understand? Imagine if every time you read Java source code and encountered syntax elements like if statements, for loops, and anonymous classes, you had to pause and puzzle over what they mean. There are certain things that must be obvious to a person who wants to be a productive Java developer. Likewise there are parts of Clojure syntax that must be obvious for one to efficiently read and understand code.

Podobnie będzie z dowolnym językiem mówionym - angielski prosty, niemiecki również, pewnie podobnie z francuskim, czy hiszpańskim, nie wspominając o rosyjskim czy w ogóle słowiańskich. Nasłuchaliśmy się ich wokoło i teraz po prostu przywykliśmy do ich konstrukcji. Brakuje nam zrozumienia ich semantyki, więc chodzimy na drogie kursy licząc, że cudem bez specjalnego wysiłku mentalnego wejdą nam w przysłowiowe 5 minut. Wtedy również zadamy sobie pytanie o sensowność nauki kolejnego języka skoro mówimy, piszemy i czytamy po angielsku. Przypomina mi to pytanie o sensowność uczenia się Clojure. Po co się go uczyć skoro znamy Javę? Po co uczyć się kolejnego języka mówionego, znając angielski? Dla mnie, odpowiedź nasuwa się sama - z ciekawości, co tracimy nie potrafiąc posługiwać się językiem, którym władają inni. Czy Clojure jest tym językiem, który należałoby poznać? Nie wiem. Wiem jednak, że należy chociażby spróbować zrozumieć programowanie funkcyjne, aby zastosować go tam, gdzie programowanie obiektowe czy imperatywne nie przystaje. Jeśli znamy tylko OO, to jak tu mówić o zdrowym rozsądku? Dobrze ujął to Abraham Maslow:

I suppose it is tempting, if the only tool you have is a hammer, to treat everything as if it were a nail.

bądź, jak to jest częściej przywoływane:

If the only tool you have is a hammer, you tend to see every problem as a nail.

Niestety nie wiem, jak miałoby to brzmieć po polsku :(

Właśnie kiedy miałem opublikować wpis, zajrzałem jeszcze do źródeł Clojure i trafiłem na funkcję clojure-version. Trywialna, aczkolwiek wciąż pouczająca implementacja (której pewnie jeszcze samodzielnie nie dałbym rady napisać):
 user=> (clojure-version)
"1.1.0-alpha-SNAPSHOT"
Jeśli doczytałeś/-aś do tego miejsca, to może i wytrwałeś/-aś z czytaniem podanego wyżej artykułu o Clojure. Jeśli tak, to w sekcji o mapach (Maps) jest wzmianka o...makrze -> (!)

The -> macro, referred to as the "thread" macro, calls a series of functions, passing the result of each as an argument to the next. For example the following lines have the same result:

(f1 (f2 (f3 x)))
(-> x f3 f2 f1)


Teraz już jest jasne, co makro -> robi. W ramach nauki Clojure pozostaje więc nauczyć się jego implementacji na pamięć, aby kolejnym razem napisać podobne.

10 listopada 2009

Środowisko programistyczne dla Clojure i poprzedni przykład funkcjonalniej

0 komentarzy
Nasze praktyczne przygody z Clojure możemy zacząć od instalacji wymaganego oprogramowania i/lub uruchomienia Clojure REPL lokalnie, albo skorzystać z Lord of the REPLs. Interesująca alternatywa, nie tylko pomocna w nauce Clojure, ale i Groovy, Ruby czy Scala.

Kiedy jednak nasze doświadczenie w programowaniu funkcyjnym w Clojure zacznie wykraczać poza proste aplikacje będące złożeniem kilku funkcji i spróbujemy wyjść poza ramy REPL możemy skorzystać ze wsparcia udostępnianego przez zintegrowane środowiska programistyczne, np. NetBeans IDE, Eclipse IDE czy IntelliJ IDEA. Wystarczy doszukać się właściwej wtyczki (rozszerzenia) i możemy czerpać garściami z pomocy IDE - możemy chociażby znieść z siebie trud dbania o nawiasy. Dla NetBeans IDE mamy Enclojure. Ostatnia wersja z 3.11, więc dostatecznie świeża, aby uwierzyć w aktywny jej rozwój. Dla środowiska Eclipse IDE mamy Clojure plug-in for Eclipse (clojure-dev), o której dowiedziałem się w artykule The Clojure programming language - Take advantage of the Clojure plug-in for Eclipse. Stawiam na NetBeans IDE i Enclojure.

Dla bloggerów istnieje podświetlanie składni dla Clojure w ramach Syntax Highlighter 2.0 - All Syntax Highlighter 2.0 brushes collected, described and downloadable. Można zobaczyć go w działaniu w moich wpisach dotyczących Clojure, chociażby ten. Jeśli więc przyjdzie zaprezentować swoje cudo w Clojure szerokiej publiczności na własnym blogu warto podeprzeć się stylem clojure. Znacznie uatrakcyjnia prezentację kodu.

A skoro mamy środowisko możemy powrócić do naszego przykładu z surowcami, której wersja bardziej przypominająca wersję funkcjonalną (dosłownie i w przenośni) wygląda teraz następująco:
 (ns pl.jaceklaskowski.clojure)

(defn czas-zwiekszenia-produkcji
"Wylicza potrzebny czas (w minutach) do osiagniecia stanu magazynowego na uruchomienie rozbudowy do poziomu przy danej produkcji"
[docelowo, magazyn, produkcja]
(let [teraz (java.util.Calendar/getInstance)]
(. teraz (add (java.util.Calendar/MINUTE)
(* 60 (reduce max (map / (map - (map #(.getValue %) docelowo) (map #(.getValue %) magazyn)) (map #(.getValue %) produkcja))))))
(. teraz getTime)))
Widać wyraźne zwiększenie konstrukcji PF z reduce i map. Poza samym wyliczeniem, za ile nastąpi zwiększenie (w linii 8.) mamy również wyliczoną dokładną godzinę, kiedy to nastąpi (linie 6., 7. i 9.)

Zamiast dłubania w REPL zapisujemy cały program w pliku czas-zwiekszenia-produkcji.clj. Zwyczajowo skrypty w Clojure zapisywane są z rozszerzeniem .clj.

Tworzymy kolejny skrypt testowy skrypt-testowy.clj, który zweryfikuje poprawność działania.
 (load-file "czas-zwiekszenia-produkcji.clj")

(ns pl.jaceklaskowski.clojure)

(def magazyn {:drewno 10 :glina 10 :zelazo 10 :zboze 10})
(def docelowo {:drewno 20 :glina 20 :zelazo 20 :zboze 20})
(def produkcja {:drewno 5 :glina 5 :zelazo 5 :zboze 5})

(println (czas-zwiekszenia-produkcji docelowo magazyn produkcja))
Uruchomienie to po prostu wykonanie:
 $ java -jar clojure-1.1.0-alpha-SNAPSHOT.jar skrypt-testowy.clj
#<Date Tue Nov 10 23:36:07 CET 2009>
I tak kończy się moje poszukiwanie ładniejszej (=bardziej funkcyjnej) wersji czas-zwiekszenia-produkcji. Oczywiście nie kończy to mojej przygody z Clojure i jeśli kiedykolwiek pomyślałem, aby sobie odpuścić, bo i po co mi PF, to wizyta na blogu BEST IN CLASS jest faktycznie najlepszym w swojej klasie i nie pozwala zapomnieć o możliwościach Clojure. Warto tam zajrzeć w wolnej chwili, szczególnie w chwilach zwątpienia. Mnie zachwycił i upewnił w postanowieniu poznania go dokładniej.

W kolejnym podejściu postaram się zrozumieć o co chodzi w makro -> oraz funkcji line-seq z core.clj jak zasugerował Michael w komentarzu do ostatniego mojego wpisu o Clojure - Programowanie w Clojure - część 2 - pierwszy przykład z wyliczaniem surowców.

08 listopada 2009

Programowanie w Clojure - część 2 - pierwszy przykład z wyliczaniem surowców

6 komentarzy
Nasze ćwiczenia z programowania w Clojure zaczniemy od uruchomienia Clojure REPL (Read-Eval-Print-Loop). Clojure REPL jest niczym innym jak interaktywnym interpreterem dla Clojure.
 jlaskowski@work /cygdrive/c/oss/clojure
$ java -jar clojure-1.1.0-alpha-SNAPSHOT.jar
Clojure 1.1.0-alpha-SNAPSHOT
user=>
Od tej pory zakładam, że wiemy, w jaki sposób dostać znak zachęty jak wyżej.

Powiedzmy, że chcemy wyliczyć liczbę surowców do wybudowania pewnej jednostki (w szczególności podniesienia poziomu samego surowca, aby dawał znacznie większą produkcję niż obecnie). Mamy 4 rodzaje surowców, które nazwiemy: drewno, glina, żelazo i zboże. Całkiem typowe w wielu grach strategicznych. Na celownik weźmiemy Traviana. Trochę czasu mi z nim zeszło i zawsze zastanawiałem się, czy jestem w stanie stworzyć system-bota, który podpowiadałby mi najlepszą strategię. Ktoś kiedyś wspominał coś o programowaniu nieliniowym odnośnie tego rodzaju kalkulacji, ale pojawiała się również wzmianka o programowaniu funkcyjnym. Ile w tym prawdy nigdy nie doszedłem, jednakże tym razem mam możliwość choć po części sprawdzić się w Clojure jako języku funkcyjnym do rozwiązania, albo chociaż próby rozwiązania, tego problemu. To byłby taki system ekspertowy, który podpowiadałby mi różne strategie (pewnie za nim stoi cała masa teorii matematycznej z różnymi modelami, ale ja wybieram podejście naiwne bez zebrania wcześniej wystarczającej wiedzy, tj. "na żywioł" :)). Do zbudowania gliny na 1. poziomie (tak, z każdą jednostką mamy związaną cechę - poziom) potrzeba 80 drewna, 40 gliny, 80 żelaza i 50 zboża (patrz 1.1.11 Kopalnia gliny). Każda wioska ma swoją produkcję i zakładam, że początkowo produkcja jest na 0. Najpierw spróbuję odpowiedzieć na pytanie, ile czasu potrzeba, abym podniósł produkcję gliny przy aktualnym stanie magazynu i produkcji surowców.

Mamy więc założenia do funkcji, którą w Javie moglibyśmy nazwać obliczCzasDoZwiekszeniaProdukcji(surowca, naPoziom, przyObecnymStanieMagazynowym, przyObecnejProdukcji). Nazwy funkcji w Clojure są zazwyczaj ciągiem słów oddzielonych myślnikami, co daje w naszym przypadku czas-zwiekszenia-produkcji (zakładając, że celem funkcji jest obliczenie czegoś nie widzę potrzeby powtarzania tego w nazwie funkcji).

Zaczynam od definicji magazynu. Będzie to mapa, która w Clojure deklarowana jest przez {} (nawiasy klamrowe), między którymi ciąg elementów tworzy pary klucz-wartość. Przecinki są opcjonalne i traktowane dokładnie jak spacja, czyli taki zapis
 user=> (def magazyn {:drewno 0 :glina 0 :zelazo 0 :zboze 0})
#'user/magazyn
jest równoznaczny takiemu:
 user=> (def magazyn {:drewno 0, :glina 0, :zelazo 0, :zboze 0})
#'user/magazyn
W definiowaniu mapy skorzystałem z możliwości definiowania słów kluczowych w Clojure z użyciem : (dwukropek). Wartością słów kluczowych są one same, więc idealnie nadają się na klucze w mapie.
 user=> :cokolwiek
:cokolwiek
Jak większość (wszystko?) w Clojure, tak i mapa jest funkcją, której pojedynczym argumentem jest klucz.
 user=> (magazyn :drewno)
0
Również odwrotne spojrzenie jest poprawne, tj. słowa kluczowe są również funkcją, której argumentem jest mapa, w której się odszukują (!)
 user=> (:drewno magazyn)
0
Takie spojrzenie na mapę i jej kluczy było dla mnie niezwykłym doświadczeniem mentalnym :) W sumie to logiczne, aby potraktować w ten sposób struktury danych, a zmienia całkowicie moje dotychczasowe spojrzenie obiektowe. Skoro wszystko jest funkcją, jedynym problemem pozostaje poznanie ich zasady działania (semantyki). Niby proste i pamiętam, że kiedy uczyłem się żonglować, co wydawało mi się początkowo niezwykle trudne i w zasadzie poza zasięgiem, kiedy oduczyłem się złego sposobu podawania piłeczki z ręki do ręki, nauczenie się poprawnie przez przerzucanie piłeczek było dziecinnie proste. Mówi się, że jedynym problemem w nauce nowego są nasze własne przyzwyczajenia i obawy. Chyba dlatego tak trudno jest się uczyć z wiekiem - zbyt wiele doświadczenia, które nas ogranicza?!

Do definicji funkcji w Clojure używamy defn, w której w cudzysłowach dodajemy opis funkcji, a w nawiasach kwadratowych parametry wejściowe.
 (defn czas-zwiekszenia-produkcji
"Wylicza potrzebny czas do zwiekszenia produkcji surowca na danym poziom przy danym stanie magazynowym i produkcji"
[surowiec, poziom, stanMagazynowy, produkcja]
...
)
Chwilowo zamiast parametru wejściowego poziom będącym liczbą całkowitą przekażemy mapę z potrzebnymi surowcami do przejścia na dany poziom.
 user=> (def poziom1 {:drewno 80 :glina 40 :zelazo 80 :zboze 50})
#'user/poziom1
W takim przypadku sam surowiec nie ma znaczenia, bo w końcu na wejściu mamy stan początkowy (stan magazynowy), stan docelowy i produkcję (przyrost jednostek w czasie 1h). Definicja funkcji mogłaby wyglądać tak:
  (defn czas-zwiekszenia-produkcji
"Wylicza potrzebny czas do osiagniecia stanu magazynowego na uruchomienie rozbudowy do poziomu przy danej produkcji"
[stanDocelowy, stanMagazynowy, produkcja]
...
)
Wszystkie parametry wejściowe są mapami.

Wywołanie funkcji w Clojure jest listą, której pierwszym elementem jest nazwa funkcji, a po nim następują parametry wejściowe.
 user=> (czas-zwiekszenia-produkcji docelowo magazyn produkcja)
Dodając do tego możliwość importowania typów javowych przez import oraz ich wywoływania przez . (kropka) czy / (ukośnik) otrzymujemy ostatecznie następujący program w Clojure do obliczania czas potrzebnego do zwiększenia produkcji surowca przy danym magazynie i bieżącej produkcji.
 (def magazyn {:drewno 10 :glina 4 :zelazo 15 :zboze 0})

(def produkcja {:drewno 5 :glina 2 :zelazo 5 :zboze 4})

(def glina1 {:drewno 80 :glina 40 :zelazo 80 :zboze 5})

(import '(java.util Calendar))

(def teraz (Calendar/getInstance))

(defn czas-zwiekszenia-produkcji
"Wylicza potrzebny czas (w minutach) do osiagniecia stanu magazynowego na uruchomienie rozbudowy do poziomu przy danej produkcji"
[docelowo, magazyn, produkcja]
(max
(/ (- (docelowo :drewno)(magazyn :drewno)) (produkcja :drewno))
(/ (- (docelowo :glina)(magazyn :glina)) (produkcja :glina))
(/ (- (docelowo :zelazo)(magazyn :zelazo)) (produkcja :zelazo))
(/ (- (docelowo :zboze)(magazyn :zboze)) (produkcja :zboze)))
)

(. teraz add (Calendar/MINUTE) (* 60 (czas-zwiekszenia-produkcji glina1 magazyn produkcja)))

(. teraz getTime)
Kopiując każdą z linii do REPL mamy:
 $ java -jar clojure-1.1.0-alpha-SNAPSHOT.jar
Clojure 1.1.0-alpha-SNAPSHOT
user=> (def magazyn {:drewno 10 :glina 4 :zelazo 15 :zboze 0})
#'user/magazyn
user=> (def produkcja {:drewno 5 :glina 2 :zelazo 5 :zboze 4})
#'user/produkcja
user=> (def glina1 {:drewno 80 :glina 40 :zelazo 80 :zboze 5})
#'user/glina1
user=> (import '(java.util Calendar))
java.util.Calendar
user=> (def teraz (Calendar/getInstance))
#'user/teraz
user=> (defn czas-zwiekszenia-produkcji
"Wylicza potrzebny czas (w minutach) do osiagniecia stanu magazynowego na uruchomienie rozbudowy do poziomu przy danej produkcji"
[docelowo, magazyn, produkcja]
(max
(/ (- (docelowo :drewno)(magazyn :drewno)) (produkcja :drewno))
(/ (- (docelowo :glina)(magazyn :glina)) (produkcja :glina))
(/ (- (docelowo :zelazo)(magazyn :zelazo)) (produkcja :zelazo))
(/ (- (docelowo :zboze)(magazyn :zboze)) (produkcja :zboze)))
)
#'user/czas-zwiekszenia-produkcji
user=> (. teraz add (Calendar/MINUTE) (* 60 (czas-zwiekszenia-produkcji glina1 magazyn produkcja)))
nil
user=> (. teraz getTime)
#<Date Mon Nov 09 07:16:59 CET 2009>
Wersja bardziej funkcyjna z map i reduce w kolejnym wpisie. Zdecydowanie zbyt wiele w tej wersji argumentów do max. Można się zapisać na śmierć z tymi kluczami w mapach i samym wprowadzaniem programu do wykonania. Wasze propozycje mile widziane, bo moja przygoda z programowaniem funkcyjnym z Clojure dopiero się zaczęła i wpadki są wręcz oczekiwane.