18 listopada 2009

Makro wplatania -> raz jeszcze i macroexpand

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?