stats

Poczytaj mi Clojure, cz. 20

Makra

Grafika

Makra to jeden z mechanizmów metaprogramowania – zbioru technik umożliwiających odczytywanie, tworzenie i modyfikowanie działających programów przez inne programy lub przez nie same. Są one jedną z charakterystycznych cech dialektów języka Lisp, pozwalając na przekształcanie kodu źródłowego programu zanim dojdzie do jego ewaluacji. Dzięki makrom możemy rozszerzać składnię języka i budować tzw. języki dziedzinowe, dostosowane do wyrażania specyficznych rozwiązań problemów w zwięzły i przejrzysty sposób.

Makra

Makro (ang. macro, z gr. makros – długi, obszerny, wielki) to skrócona nazwa terminu makroinstrukcja (ang. macroinstruction) użytego po raz pierwszy w publikacji zatytułowanej „The Share 709 System: Programming and Modification”, wydanej w Nowym Jorku w roku 1959. Opisuje ona m.in. specjalne instrukcje assemblera, które nie były typowymi mnemonikami, dającymi się bezpośrednio przełożyć na instrukcje kodu maszynowego, lecz służyły do generowania zestawów innych instrukcji.

Makroinstrukcje te przypominały więc szablony w stylu kopiuj–wklej. Bez nich programista musiałby wielokrotnie wprowadzać te same długie sekwencje rozkazów, aby wyrażać często przeprowadzane operacje na danych. Z czasem zaczęto powszechnie używać skrótowego określenia makro w odniesieniu do różnorakich konstrukcji języków programowania pozwalających na generowanie lub modyfikowanie kodu źródłowego.

Makra mogą być implementowane jako zestawy reguł lub wzorców określających przebieg procesu modyfikacji kodu, czyli tego, w jaki sposób ujęty w wywołanie makra fragment ma być przekształcany bądź wstawiany. W językach kompilowanych makra uruchamiane są zanim dojdzie do właściwej kompilacji i konsolidacji.

Spójrzmy na proste makro preprocesora z języka C:

 1#include <stdio.h>
 2
 3#define non_zero(x) ((x) > 0)
 4
 5int main(int args, char *argv[]) {
 6  int a = 5;
 7
 8  if (non_zero(a)) {
 9    printf("a jest większe od zera\n");
10  }
11
12  return 0;
13}
#include &lt;stdio.h&gt; #define non_zero(x) ((x) &gt; 0) int main(int args, char *argv[]) { int a = 5; if (non_zero(a)) { printf(&#34;a jest większe od zera\n&#34;); } return 0; }

Dyrektywa #define umieszczona w linii nr 3 mówi, że gdziekolwiek pojawi się fragment tekstu kodu źródłowego non_zero(), jego część ujęta w nawiasy powinna być podstawiona przed stałym zapisem > 0, a rezultat powinien zastąpić oryginalny fragment. Kiedy więc pojawi się (jak w linii nr 8 naszego przykładu) zapis non_zero(a), zostanie zastąpiony przez a > 0. Gdybyśmy napisali na przykład non_zero(2+2), powstałoby 2+2 > 0.

W języku C system obsługi makr preprocesora bazuje na sztywnym zestawie reguł umożliwiającym przeprowadzanie prostych operacji na kodzie źródłowym. Okazuje się jednak, że możliwe jest nieco inne podejście, lecz wymaga ono języka o innej konstrukcji.

Makra Clojure

Makra w Clojure operują na składni programu (ale nie na jej formie tekstowej!) i w użytkowaniu przypominają funkcje: można je definiować, wywoływać, przyjmują argumenty i zwracają wartości. Różnią się jednak tym, że ich podprogramy są realizowane podczas wstępnego etapu kompilacji, zanim dojdzie do generowania kodu bajtowego przeznaczonego do uruchamiania, ale po analizie składniowej. Wspólnym mianownikiem pozostaje fakt, że takie specjalne „funkcje” nie będą przetwarzały danych programu, ale fragmenty jego kodu źródłowego.

Mamy więc do czynienia z pewną fazą przetwarzania kodu źródłowego, w której makra mogą go zmienić, ale w odróżnieniu od zaprezentowanej makrodefinicji z języka C nie dokonują tego przez proste manipulowanie wersją tekstową (podstawianie, dodawanie i usuwanie fragmentów), lecz przez przetwarzanie struktur danych reprezentujących kod źródłowy w pamięci.

Przypomnijmy, że proces powstawania wynikowej postaci programu i jego późniejszego uruchamiania w większości kompilatorów przebiega wg. następujących kroków:

  1. analiza leksykalna,
  2. analiza składniowa,
  3. analiza semantyczna,
  4. generowanie i optymalizacja kodu wynikowego,
  5. konsolidacja kodu wynikowego,
  6. uruchamianie kodu wynikowego.

W przypadku Clojure będzie to przekładało się na następujące etapy (z podziałem na lispowe komponenty uczestniczące w procesie):

  1. Czytnik:

    1. Wczytanie tekstu kodu źródłowego (analiza leksykalna).
    2. Zmiana S-wyrażeń w obiekty drzewa składniowego (analiza składniowa).
    3. Rozwijanie makr i aktualizacja drzewa składniowego.
  2. Ewaluator:

    1. Rozpoznawanie form (analiza semantyczna).
    2. Generowanie kodu bajtowego.
    3. Uruchamianie kodu bajtowego i wartościowanie form.

Argumentami makr będą formy przed ich ewaluacją, czyli pamięciowe reprezentacje S-wyrażeń, które powstały w efekcie analizy składniowej. Wartościami zwracanymi przez makra również będą formy, czyli struktury danych odzwierciedlające kod, które zostaną włączone do reprezentacji znajdującej się w drzewie składniowym.

Dotykamy w tym miejscu ciekawej zależności. Okazuje się, że w większości języków programowania reprezentacje kodu źródłowego w pamięci korzystają ze struktur tworzonych w innym języku, np. w C czy C++, w którym napisany jest jego kompilator bądź interpreter. Z kolei sam program, niezależnie od fazy realizacji procesu kompilacji czy interpretacji, nie ma do nich dostępu – stanowią dane wewnętrzne, niedostępne programiście.

W Lispach, dzięki ich homoikoniczności, jest inaczej. Pobierając wyrażenia drzewa składniowego w fazie rozwijania makr, de facto jesteśmy w stanie odtworzyć czytelny kod źródłowy w postaci tekstowej.

Gdy spojrzymy na wywołania niektórych makr języka Clojure, na pierwszy rzut oka nie można rozróżnić ich od zwykłych funkcji czy form specjalnych:

Przykłady wywoływania wbudowanych makr języka Clojure
1(and     (= 2 2) (< 1 2) "prawda")  ; => "prawda"
2(if-let  [x 5] x)                   ; => 5
3(dotimes [x 2] (println x))         ; => nil  >> 0 1
4(doto    1 println)                 ; => 1    >> 1
(and (= 2 2) (&lt; 1 2) &#34;prawda&#34;) ; =&gt; &#34;prawda&#34; (if-let [x 5] x) ; =&gt; 5 (dotimes [x 2] (println x)) ; =&gt; nil &gt;&gt; 0 1 (doto 1 println) ; =&gt; 1 &gt;&gt; 1

Widać tu różnicę między użyciem #define z języka C, gdzie korzystaliśmy ze specyficznej składni przynależącej do tzw. języka makrowego (ang. macro language), zwanego też makrojęzykiem, którego syntaktyka jest różna od tej, z której skorzystamy do pracy z danymi dostępnymi uruchamianemu programowi.

W przypadku Clojure (i innych Lispów) tej samej składni użyjemy do operowania na danych użytkowych, jak i na danych wyrażających kod źródłowy (w makrach). Innymi słowy makrojęzykiem w Clojure jest również Clojure.

Oczywiście trudno przeoczyć fakt, że w makrach nie uzyskamy dostępu do tych samych danych użytkowych, które „widzi” uruchomiony program, ponieważ na tym etapie nie będzie on jeszcze działać.

W traktowaniu kodu źródłowego tak samo jak danych, na których operuje się z użyciem standardowych konstrukcji językowych, czynny udział ma zapętlenie faz tłumaczenia postaci źródłowej programu do jego formy wykonywalnej. W niektórych przypadkach będziemy mieli makra, które operują na innych makrach – będzie więc wiele abstrakcyjnych warstw składniowego przetwarzania programu.

Korzystanie z makr

Korzystanie z makr polega na ich tworzeniu i wywoływaniu. Tworzenie wymaga użycia konstrukcji służących do definiowania makr, a wywoływanie wygląda tak samo, jak wywoływanie funkcji: korzystając z listowego S-wyrażenia, podajemy na jego pierwszej pozycji nazwę makra, a następnie przyjmowane przez nie argumenty, czyli w tym przypadku symboliczne wyrażenia (fragmenty kodu źródłowego).

Makroekspansja

Formy wywołania makr będą wartościowane zanim dojdzie do obliczania wartości pozostałych form wyrażonych w programie, w fazie tzw. rozwijania makr (ang. macro-expansion) lub inaczej makroekspansji (ang. macroexpansion).

Możemy zauważyć, że rozwijanie makr następuje po wczytaniu kodu do pamięci (odzwierciedleniu symbolicznych wyrażeń w AST), ale przed jego wartościowaniem.

Tworzenie makr

Tworzenie lispowych makr polega na definiowaniu specyficznych konstrukcji przypominających funkcje. Ich zadaniem będzie przekształcanie form, czyli przekazanych w argumentach struktur danych, które reprezentują poprawne S-wyrażenia kodu źródłowego przeznaczone do późniejszej ewaluacji.

Argumenty makr

Parametrami makr, czyli przekazywanymi w trakcie makroekspansji wartościami argumentów, będą zawsze formy stałe. Tak jakby S-wyrażenia umieszczane na kolejnych pozycjach formy wywołania makra zostały zacytowane.

Na przykład, jeżeli przekażemy niezacytowany symbol, wtedy w parametrze widocznym w ciele makra nie będzie on reprezentowany wartością wskazywaną tym symbolem, ponieważ na tym etapie kompilacji nie możemy jeszcze sięgnąć do danych użytkowych programu, lecz jego formą stałą.

Podobnie z wyrażeniami złożonymi:

Jeżeli sobie tego zażyczymy, będziemy mogli dla wybranych parametrów dokonać ich wymuszonego wartościowania – służą do tego odpowiednie formy specjalne. Należy jednak mieć na uwadze, że z natychmiastowym wartościowaniem przekazanego S-wyrażenia w fazie makroekspansji nie będziemy mieli do czynienia niemalże nigdy, ponieważ na tym etapie przetwarzania kontekst uruchamiania nie zawiera żadnych danych dostępnych uruchamianemu programowi. Często natomiast wstawimy parametr w odpowiednie miejsce kodu produkowanego przez makro, aby był wartościowany później, podczas ewaluacji form programu.

Żeby zobaczyć jakim typem danych wyrażana będzie dana forma składniowa, można zajrzeć do tabeli form czytnika w rozdziale nr III.

Wartości zwracane makr

Obsługa wartości zwracanych przez makra również nieco różni się od tego, z czym mamy do czynienia w funkcjach. Oczekuje się, że wartością ostatniego wyrażenia obliczanego w ciele makra będzie struktura danych zawierająca formę stałą stanowiącą reprezentację kodu źródłowego przeznaczonego do ewaluacji. Może nią być np. lista, mapa, wektor, zbiór bądź pojedyncza wartość (m.in. symbol, klucz, liczba czy łańcuch znakowy).

Zwrócona przez makro struktura danych zostanie wkomponowana w miejsce drzewa składniowego, w którym znajdowała się forma wywołania makra, aby reprezentować nowy kod. Będziemy mieli do czynienia z procesem analogicznym do zastąpienia S-wyrażenia w danym miejscu tekstu programu.

Definiowanie makr, defmacro

Makro defmacro pozwala definiować makra w podobny sposób, w jaki defn umożliwia tworzenie nazwanych funkcji. Do makr również można odwoływać się z użyciem symbolicznych identyfikatorów, a odwołania te przechowywane są w zmiennych globalnych (obiektah typu Var internalizowanych w przestrzeniach nazw).

Użycie:

  • (defmacro nazwa dok? metadane?  parametry & wyrażenie…),
  • (defmacro nazwa dok? metadane? (parametry & wyrażenie…)… metadane?);

gdzie poszczególne etykiety mają następujące znaczenia:

Dokładne informacje dotyczące argumentów wywołania i konstruowania wektorów z parametrami znajdują się w sekcji „Argumenty funkcji” rozdziału VIII.

Pierwszym argumentem wywołania defmacro powinna być nazwa makra wyrażona symbolem. Następnym, opcjonalnym argumentem może być łańcuch dokumentujący, a kolejnym (również nieobowiązkowym) mapa metadanowa zawierająca metadane, które zostaną ustawione dla zmiennej globalnej odwołującej się do obiektu makra. Nic nie stoi też na przeszkodzie, aby metadane były ustawione w symbolu określającym nazwę makra zamiast w osobnej mapie – zostaną wtedy skopiowane do obiektu typu Var odnoszącego się do makra.

Po nazwie i opcjonalnym łańcuchu dokumentującym i/lub metadanych należy podać wektor parametryczny, który określa przyjmowane argumenty. W wektorze możemy korzystać z destrukturyzacji.

Kolejne przekazywane do defmacro argumenty to wyrażenia składające się na ciało makra.

Podczas realizowania makra w fazie makroekspansji podane argumenty nie będą wartościowane, lecz odpowiadające im parametry zostaną powiązane z reprezentującymi je w drzewie składniowym strukturami pamięciowymi wyrażającymi formy przeznaczone do ewaluacji. Jeżeli na przykład jako argument makra podamy S-wyrażenie (+ 2 2), wartością, z którą powiązany zostanie parametr, będzie lista dająca się symbolicznie wyrazić jako '(clojure.core/+ 2 2). Gdybyśmy taki sam argument przekazali do funkcji, wartością parametru byłaby liczba 4, bo przed przekazaniem obliczona zostałaby wartość wyrażenia.

Wartość ostatnio obliczonego wyrażenia umieszczonego w ciele makra zostanie umieszczona w drzewie składniowym, w miejscu wywołania makra, a w fazie wartościowania wyrażeń poddana będzie procesowi ewaluacji. Rezultat tego wartościowania z perspektywy uruchamianego programu możemy potocznie nazwać wartością zwracaną przez makro.

Podobnie jak w przypadku formy specjalnej fn możemy podać wiele argumentowości i wiele ciał makr. Każde z nich powinno być w takim przypadku zapisane jako listowe S-wyrażenie, którego pierwszym elementem jest wektor parametryczny. Będziemy wtedy mieć do czynienia z tzw. makrem wieloczłonowym, które przypomina funkcję wieloczłonową (zwaną też wieloargumentowościową). Podczas wywołania makra w fazie makroekspansji za transformację będzie odpowiadało to ciało, do którego wektora parametrycznego pasuje przekazany zestaw argumentów.

Makro defmacro zwraca obiekt typu funkcyjnego. Efektem ubocznym jest utworzenie identyfikowanej symbolem zmiennej globalnej w bieżącej przestrzeni nazw, która będzie powiązana z tym obiektem. Dodatkowo, konstytuujący zmienną obiekt typu Var będzie miał ustawiony znacznik metadanowy :macro na wartość true. Dzięki temu mechanizmy kompilatora są w stanie rozróżnić, że nie mają do czynienia ze zwykłą funkcją, lecz właśnie z makrem.

Przykłady użycia makra defmacro
 1(defmacro do-tekstu
 2  "Zwraca tekstową reprezentację podanego wyrażenia."
 3  [wyrażenie]
 4  (str "WYWOŁANO: " wyrażenie))
 5
 6(do-tekstu (+ 1 2 3))    ; => "WYWOŁANO: (+ 1 2 3)"
 7(do-tekstu (+ 1, 2, 3))  ; => "WYWOŁANO: (+ 1 2 3)"
 8(do-tekstu "test")       ; => "WYWOŁANO: test"
 9
10(defmacro odwróć-argumenty
11  "Odwraca argumenty podanych wyrażeń."
12  [& wyrażenia]
13  (cons 'do (map #(cons (first %1) (reverse (rest %1))) wyrażenia)))
14
15(odwróć-argumenty
16 (println 1 2 3)
17 (vector  1 2 3))
18
19; >> 3 2 1
20; => [3 2 1]
(defmacro do-tekstu &#34;Zwraca tekstową reprezentację podanego wyrażenia.&#34; [wyrażenie] (str &#34;WYWOŁANO: &#34; wyrażenie)) (do-tekstu (+ 1 2 3)) ; =&gt; &#34;WYWOŁANO: (+ 1 2 3)&#34; (do-tekstu (+ 1, 2, 3)) ; =&gt; &#34;WYWOŁANO: (+ 1 2 3)&#34; (do-tekstu &#34;test&#34;) ; =&gt; &#34;WYWOŁANO: test&#34; (defmacro odwróć-argumenty &#34;Odwraca argumenty podanych wyrażeń.&#34; [&amp; wyrażenia] (cons &#39;do (map #(cons (first %1) (reverse (rest %1))) wyrażenia))) (odwróć-argumenty (println 1 2 3) (vector 1 2 3)) ; &gt;&gt; 3 2 1 ; =&gt; [3 2 1]

Zauważmy, że w linii nr 7 przekazujemy do wywołania makra listowe S-wyrażenie (+ 1, 2, 3), którego elementy oddzielone zostały separatorem w postaci przecinka. Kiedy jednak makro zwraca wartość w postaci łańcucha znakowego (rezultat wywołania funkcji str na strukturze wejściowej), przecinków nie ma: (+ 1 2 3).

Jest to potwierdzenie tego, że wartości argumentów makr nie są po prostu tekstową reprezentacją fragmentów kodu źródłowego, ale elementami składniowego drzewa stworzonego z rozpoznanych wcześniej S-wyrażeń.

W naszym przykładzie listowe S-wyrażenie przekazane jako argument jest w pamięci reprezentowane w postaci listy, a dokładnie obiektu typu clojure.lang.PersistentList. Na liście tej możemy dokonywać różnych przekształceń, a także odczytywać jej zawartość, korzystając z rozmaitych funkcji języka.

Rezultatem (ostatnim obliczanym wyrażeniem makra) musi być taka struktura danych, której elementy wyrażają kod źródłowy.

W przypadku listy możemy na jej pierwszym miejscu umieścić nazwę symbolu odnoszącego się do jakiejś funkcji, makra lub formy specjalnej, a pozostałe elementy potraktować jak przekazywane argumenty. Gdy będzie ona potem wartościowana, dojdzie do wywołania odpowiedniego podprogramu – zupełnie tak samo, jak przy listowym S-wyrażeniu umieszczonym w danym miejscu kodu.

W naszym konkretnym przykładzie (linia nr 4) nie tworzymy listy, ale pojedynczą wartość: łańcuch znakowy, w którym umieściliśmy tekstową reprezentację struktury danych wyrażającej kod podany jako argument makra. Po przeliczeniu wartością będzie więc ten sam łańcuch znakowy, ponieważ mamy do czynienia z formą stałą.

Zwróćmy jeszcze uwagę na znacznik cytowania w linii nr 13. Budujemy tam listę, która po zakończeniu pracy makra stanie się kodem źródłowym. Będzie ona strukturą pochodną względem podanej jako argument przy czym kolejność wszystkich oprócz ostatnich elementów w każdej z list wejściowych zostanie odwrócona. Problemem jest jednak to, że mamy tam do czynienia z wieloma wyrażeniami, które trzeba jakoś zgrupować. W tym celu do nadrzędnej listy jako pierwszy element wstawiamy (z użyciem conj) symbol do. Musimy go zacytować, bo w przeciwnym razie zostałby on podczas działania makra przeliczony, zamiast reprezentować formę specjalną do wskazaną symbolem.

Zobacz także:

Cytowanie składniowe, `

Pojedynczy symbol `, czyli grawis (ang. grave accent), zwany też symbolem odwróconego pojedynczego cudzysłowu (ang. backquote) lub odwróconej fajki (ang. backtick), jest nazwą makra czytnika, które sprawia, że umieszczone po nim S-wyrażenie będzie rekurencyjnie zacytowane z użyciem tzw. cytowania składniowego.

Użycie:

  • `wyrażenie.

Cytowanie składniowe (ang. syntax-quote) różni się od konwencjonalnego cytowania (wyrażanego znakiem pojedynczego apostrofu) tym, że możemy w nim korzystać ze specjalnych znaczników, które dla wybranych S-wyrażeń, wchodzących w skład zacytowanego S-wyrażenia, pozwalają:

Cytowanie składniowe działa dla S-wyrażeń, które w pamięci reprezentowane są z użyciem symboli, list, wektorów, zbiorówmap. Wszystkie inne konstrukcje językowe będą pozostawione w postaci niezacytowanej (potraktowane jak wartości stałe).

Poddane cytowaniu składniowemu symbole będą przekształcane do form stałych z dookreślonymi przestrzeniami nazw. Wyjątek stanowiły będą symbole, do których dodano (opisany dalej) specjalny znacznik # – w ich przypadku dojdzie do wygenerowania w danych miejscach symboli o unikatowych nazwach.

Poddane cytowaniu składniowemu kolekcje reprezentujące złożone S-wyrażenia (listy, wektory, mapy i zbiory) zostaną rekurencyjnie zacytowane, przy czym w procesie tym zastosowanie będą mogły znaleźć wspomniane wcześniej specjalne znaczniki wyłączające cytowanie dla wybranych elementów.

Z cytowania składniowego skorzystamy zazwyczaj w makrach, ale nic nie stoi na przeszkodzie, aby używać go w dowolnym innym miejscu programu, jeżeli mamy taką potrzebę.

Przykłady użycia cytowania składniowego
1(def a :a) (def b :b) (def c :c)
2
3`(1 2 3)               ; => (1 2 3)
4`(vector a b)          ; => (clojure.core/vector user/a user/b)
5`(vector 1 2 3 [a b])  ; => (clojure.core/vector 1 2 3 [user/a user/b])
6`[a b 1 2 3]           ; => [user/a user/b 1 2 3]
(def a :a) (def b :b) (def c :c) `(1 2 3) ; =&gt; (1 2 3) `(vector a b) ; =&gt; (clojure.core/vector user/a user/b) `(vector 1 2 3 [a b]) ; =&gt; (clojure.core/vector 1 2 3 [user/a user/b]) `[a b 1 2 3] ; =&gt; [user/a user/b 1 2 3]

Zauważmy, że zastosowanie cytowania składniowego w odniesieniu do symboli prowadzi do wygenerowania ich stałych form z dookreślonymi przestrzeniami nazw. Dzięki temu unikamy konfliktów identyfikatorów w fazie obliczania wartości wyrażenia zwróconego przez makro lub podczas wartościowania wybranych wyrażeń z użyciem wspomnianych wcześniej, specjalnych znaczników.

Sprawdźmy jeszcze z jakimi typami danych mamy dokładnie do czynienia, gdy stosujemy składniowe cytowanie S-wyrażeń:

Przykłady użycia cytowania składniowego
 1(defn jaki-typ [wyr]
 2  (list
 3   (symbol (last (clojure.string/split (str (type wyr)) #"\.")))
 4   (if (coll? wyr) (map jaki-typ wyr) wyr)))
 5
 6(jaki-typ `(1 2 3))
 7; => (Cons                           ; komórki Cons
 8; =>  ((Long 1)                      ; liczba całkowita
 9; =>   (Long 2)                      ; liczba całkowita
10; =>   (Long 3)))                    ; liczba całkowita
11
12(jaki-typ `(vector a b))
13; => (Cons                           ; komórki Cons
14; =>  ((Symbol clojure.core/vector)  ; symbol
15; =>   (Symbol user/a)               ; symbol
16; =>   (Symbol user/b)))             ; symbol
17
18(jaki-typ `(vector 1 2 3 [a b]))
19; => (Cons                           ; komórki Cons
20; =>  ((Symbol clojure.core/vector)  ; symbol
21; =>   (Long 1)                      ; liczba całkowita
22; =>   (Long 2)                      ; liczba całkowita
23; =>   (Long 3)                      ; liczba całkowita
24; =>   (PersistentVector             ; wektor
25; =>    ((Symbol user/a)             ; symbol
26; =>     (Symbol user/b)))))         ; symbol
27
28(jaki-typ `[a b 1 2 3])
29; => (PersistentVector               ; wektor
30; =>  ((Symbol user/a)               ; symbol
31; =>   (Symbol user/b)               ; symbol
32; =>   (Long 1)                      ; liczba całkowita
33; =>   (Long 2)                      ; liczba całkowita
34; =>   (Long 3)))                    ; liczba całkowita
(defn jaki-typ [wyr] (list (symbol (last (clojure.string/split (str (type wyr)) #&#34;\.&#34;))) (if (coll? wyr) (map jaki-typ wyr) wyr))) (jaki-typ `(1 2 3)) ; =&gt; (Cons ; komórki Cons ; =&gt; ((Long 1) ; liczba całkowita ; =&gt; (Long 2) ; liczba całkowita ; =&gt; (Long 3))) ; liczba całkowita (jaki-typ `(vector a b)) ; =&gt; (Cons ; komórki Cons ; =&gt; ((Symbol clojure.core/vector) ; symbol ; =&gt; (Symbol user/a) ; symbol ; =&gt; (Symbol user/b))) ; symbol (jaki-typ `(vector 1 2 3 [a b])) ; =&gt; (Cons ; komórki Cons ; =&gt; ((Symbol clojure.core/vector) ; symbol ; =&gt; (Long 1) ; liczba całkowita ; =&gt; (Long 2) ; liczba całkowita ; =&gt; (Long 3) ; liczba całkowita ; =&gt; (PersistentVector ; wektor ; =&gt; ((Symbol user/a) ; symbol ; =&gt; (Symbol user/b))))) ; symbol (jaki-typ `[a b 1 2 3]) ; =&gt; (PersistentVector ; wektor ; =&gt; ((Symbol user/a) ; symbol ; =&gt; (Symbol user/b) ; symbol ; =&gt; (Long 1) ; liczba całkowita ; =&gt; (Long 2) ; liczba całkowita ; =&gt; (Long 3))) ; liczba całkowita

Widzimy jakie struktury i typy danych odpowiadają poszczególnym elementom S-wyrażeń, które zostały zacytowane. Z takimi samymi strukturami będziemy mieć do czynienia podczas przetwarzania argumentów makr.

Transponowanie S-wyrażeń, ~

Znacznik ~ (tylda) to makro czytnika, które w cytowaniu składniowym wyraża operację transponowania lub odwrotnego cytowania (ang. unquote). Służy do wskazywania tych form, których wartość ma zostać obliczona w fazie ewaluacji, mimo że wyrażające je, obejmujące S-wyrażenia zostały zacytowane.

Użycie:

  • ~ wyrażenie.

Znacznik należy umieścić przed S-wyrażeniami, których ma dotyczyć (znak spacji można pominąć). Użycie go sprawi, że ich formy zostaną wyłączone z procesu cytowania składniowego i w ich miejscu pojawią się wartości będące efektem obliczenia wyrażeń. Dokładniej rzecz ujmując, wyrażenia te zostaną na etapie makroekspansji odpowiednio oznaczone i potem, w fazie ewaluacji przeliczone do wartości.

Przypomina to mechanizm szablonów (ang. templates) znany z generatorów stron WWW czy skryptów do automatycznego odpowiadania na e-maile. Możemy określać pewne fragmenty, które będą dynamicznie podstawiane.

Przykłady użycia cytowania składniowego z transponowaniem S-wyrażeń
 1;; definiujemy zmienne globalne identyfikowane symbolami a, b i c:
 2
 3(def a :a) (def b :b) (def c :c)
 4
 5;; transponujemy zagnieżdżone wektorowe S-wyrażenie:
 6
 7`(vector 1 2 3 ~[a b c])
 8; => (clojure.core/vector 1 2 3 [:a :b :c])
 9
10;; transponujemy całe listowe S-wyrażenie:
11
12`~(list a b c)
13; => (:a :b :c)
14
15;; transponujemy dwa pierwsze symbole wektorowego S-wyrażenia:
16
17`[~a ~b c 1 2 3]
18; => [:a :b user/c 1 2 3]
19
20;; transponujemy wybrane symbole zagnieżdżonego, listowego S-wyrażenia:
21
22(defmacro sumuj [a b] `(+ ~a ~b))
23(sumuj 1 2)
24; => 3
;; definiujemy zmienne globalne identyfikowane symbolami a, b i c: (def a :a) (def b :b) (def c :c) ;; transponujemy zagnieżdżone wektorowe S-wyrażenie: `(vector 1 2 3 ~[a b c]) ; =&gt; (clojure.core/vector 1 2 3 [:a :b :c]) ;; transponujemy całe listowe S-wyrażenie: `~(list a b c) ; =&gt; (:a :b :c) ;; transponujemy dwa pierwsze symbole wektorowego S-wyrażenia: `[~a ~b c 1 2 3] ; =&gt; [:a :b user/c 1 2 3] ;; transponujemy wybrane symbole zagnieżdżonego, listowego S-wyrażenia: (defmacro sumuj [a b] `(+ ~a ~b)) (sumuj 1 2) ; =&gt; 3

Transponowanie z rozplataniem, ~@

Znacznik ~@ (tylda i małpka) to makro czytnika, które w cytowaniu składniowym wyraża operację transponowania z rozplataniem, zwaną też transponowaniem ze spłaszczaniem lub odwrotnym cytowaniem z rozplataniem (ang. unquote-splice).

Ten rodzaj transponowania działa podobnie do omówionego wcześniej, lecz zastosować go możemy tylko w odniesieniu do złożonych S-wyrażeń, reprezentowanych w parametrach makra listą, wektorem, mapą lub zbiorem.

Użycie:

  • ~@ wyrażenie-złożone.

Elementy wyrażenia poprzedzonego znacznikiem transponowania z rozplataniem zostaną przeniesione do struktury wyrażenia nadrzędnego (obejmującego), stając się jego elementami, a dodatkowo oznaczone jako formy przeznaczone do wartościowania w fazie ewaluacji.

Przydaje się to na przykład w obsłudze makr, które operują na parametrze wariadycznym.

Przykłady użycia cytowania składniowego i transponowania z rozplataniem
 1(def a :a) (def b :b) (def c :c)
 2
 3`(vector 1 2 3 ~@[a b c])
 4; => (clojure.core/vector 1 2 3 :a :b :c)
 5
 6`(~@(list a b c))
 7; => (:a :b :c)
 8
 9`(a b c ~@[1 2 3])
10; => (user/a user/b user/c 1 2 3)
11
12`(1 2 3 ~@(list (+ 2 2)))
13; => (1 2 3 4)
14
15(defmacro sumuj [& args] `(+ ~@args))
16(sumuj 1 2 3 4)
17; => 10
(def a :a) (def b :b) (def c :c) `(vector 1 2 3 ~@[a b c]) ; =&gt; (clojure.core/vector 1 2 3 :a :b :c) `(~@(list a b c)) ; =&gt; (:a :b :c) `(a b c ~@[1 2 3]) ; =&gt; (user/a user/b user/c 1 2 3) `(1 2 3 ~@(list (+ 2 2))) ; =&gt; (1 2 3 4) (defmacro sumuj [&amp; args] `(+ ~@args)) (sumuj 1 2 3 4) ; =&gt; 10

Spójrzmy jeszcze na przykład makra, w którym transponowanie z rozplataniem pomaga przetwarzać parametr wariadyczny, zawierający wiele reprezentacji S-wyrażeń przekazanych jako argumenty:

Przykład transponowania z rozplataniem w definicji makra
 1(defmacro rób-parzyste
 2  "Oblicza wartości parzystych wyrażeń podanych jako argumenty
 3  i zwraca wartość ostatniego."
 4  [& wyrażenia]
 5  `(do
 6     ~@(take-nth 2 (rest wyrażenia))))
 7
 8(rób-parzyste
 9 (println "pierwsze")
10 (println "drugie")
11 (+ 2 2)
12 "koniuszek")
13
14; >> drugie
15; => "koniuszek"
(defmacro rób-parzyste &#34;Oblicza wartości parzystych wyrażeń podanych jako argumenty i zwraca wartość ostatniego.&#34; [&amp; wyrażenia] `(do ~@(take-nth 2 (rest wyrażenia)))) (rób-parzyste (println &#34;pierwsze&#34;) (println &#34;drugie&#34;) (+ 2 2) &#34;koniuszek&#34;) ; &gt;&gt; drugie ; =&gt; &#34;koniuszek&#34;

Generowanie unikatowych symboli, #

Jeżeli wewnątrz zacytowanego składniowo S-wyrażenia pojawi się symboliczna nazwa zakończona znakiem # (kratka) i nie będzie miała ona dookreślonej przestrzeni nazw, wtedy taki zapis nie zostanie potraktowany jak reprezentacja symbolu i zacytowany, lecz w jego miejsce podstawiony będzie rezultat jednokrotnego (dla podanej nazwy) wywołania funkcji gensym, która służy do tworzenia unikatowych symboli.

Użycie:

  • symbol#.

Znacznik generatora symbolu umieszczamy za jego nazwą (dołączając go do niej). Sprawi on, że wygenerowany zostanie symbol o unikatowej nazwie, stworzonej przez dodanie do podanej łańcuchów znakowych i losowo wybranych cyfr.

Generowanie symboli w makrach pozwala na bezpieczne korzystanie z nich w konstruowanych reprezentacjach S-wyrażeń, ponieważ pomaga unikać nieoczekiwanych przesłonięć identyfikatorów pochodzących z wyrażeń je obejmujących.

Spójrzmy na prosty przykład zacytowanego wywołania konstrukcji let:

1`(let [a 2] a)
2; => (clojure.core/let [user/a 2] user/a)
`(let [a 2] a) ; =&gt; (clojure.core/let [user/a 2] user/a)

Rezultatem jest lista, której pierwszy element jest symbolem nazywającym formę specjalną let (z ustawioną przestrzenią nazw clojure.core), drugi to wektor powiązań, a ostatnim jest symbol a z przestrzenią nazw user.

Co by się stało, gdybyśmy chcieli wartościować powyższe wyrażenie, bo byłoby ono rezultatem pracy makra?

1(defmacro pisz-dwa []
2  `(let [a 2]
3     a))
4
5(pisz-dwa)
6; >> java.lang.RuntimeException: Can't let qualified name: user/a
(defmacro pisz-dwa [] `(let [a 2] a)) (pisz-dwa) ; &gt;&gt; java.lang.RuntimeException: Can&#39;t let qualified name: user/a

Zgłoszony wyjątek informuje nas o tym, że nie można użyć symbolu z dookreśloną przestrzenią nazw w wektorze powiązań. Musimy więc znaleźć sposób na tworzenie symboli na użytek powiązań leksykalnych w generowanych wyrażeniach. Możemy to zrobić na przykład z wykorzystaniem funkcji symbol, która na podstawie łańcucha znakowego wygeneruje symbol w momencie wartościowania wyrażenia:

1(defmacro pisz-dwa []
2  `(let [~(symbol "a") 2]
3     ~(symbol "a")))
4
5(pisz-dwa)
6; => 2
(defmacro pisz-dwa [] `(let [~(symbol &#34;a&#34;) 2] ~(symbol &#34;a&#34;))) (pisz-dwa) ; =&gt; 2

Inna metoda to użycie transponowania w odniesieniu do zacytowanych symboli:

1(defmacro pisz-dwa []
2  `(let [~'a 2]
3     ~'a))
4
5(pisz-dwa)
6; => 2
(defmacro pisz-dwa [] `(let [~&#39;a 2] ~&#39;a)) (pisz-dwa) ; =&gt; 2

Sposób ten zadziała, jednak po pierwsze zapis będzie mniej czytelny, a po drugie: jeżeli gdziekolwiek w wyrażeniu wprowadzanym do makra (zakładając, że argumenty makra są aktywnie używane) znajdzie się symbol o takiej samej nazwie, jak ta użyta przez nas, możemy mieć kłopoty, jeżeli w trakcie wartościowania nazwa zostanie przesłonięta przez powiązanie z inną wartością. Popatrzmy:

1(defmacro dodaj-dwa [x]
2  `(let [~'a 2]
3     (+ ~'a ~x)))
4
5(dodaj-dwa (inc 1))
6; => 4
7
8(let [a 10] (dodaj-dwa a))
9; => 4           ; ???!
(defmacro dodaj-dwa [x] `(let [~&#39;a 2] (+ ~&#39;a ~x))) (dodaj-dwa (inc 1)) ; =&gt; 4 (let [a 10] (dodaj-dwa a)) ; =&gt; 4 ; ???!

Spodziewaliśmy się, że ostatnie wywołanie zwróci wartość 12, a uzyskaliśmy 4. Dlaczego? Wartością rozwinięcia ostatniego wywołania makra (linia nr 9) było wyrażenie, którego symboliczna postać wygląda następująco:

1(let* [a 2]
2  (clojure.core/+ a a))
(let* [a 2] (clojure.core/+ a a))

Uwzględniając leksykalne otoczenie, będziemy mieli do czynienia z następującym kodem:

1(let [a 10]
2  (let* [a 2]
3    (clojure.core/+ a a)))
(let [a 10] (let* [a 2] (clojure.core/+ a a)))

Podany w wywołaniu (dodaj-dwa a) symbol a został zgodnie z oczekiwaniami podstawiony w miejsce ~x. Pamiętajmy, że mamy do czynienia z makrem, więc argumentem wejściowym (parametr x) nie będzie wartość, z którą przekazywany symbol powiązano, lecz jego własna reprezentacja, ponieważ wartościowanie wyrażeń kodu źródłowego jeszcze się nie rozpoczęło.

Efektem pracy makra będzie więc wygenerowanie wyrażenia, w którym najpierw dochodzi do powiązania symbolu a z wartością 2, a następnie do sumowania wartości tego symbolu z wartością symbolu przekazanego z zewnątrz: (clojure.core/+ a a).

Ponieważ jednak generowany przez makro symbol ma taką samą nazwę, jak nazwa symbolu przekazana do makra, w momencie wartościowania dojdzie do konfliktu. Generowane przez makro wyrażenie dokonało powiązania symbolu a z wartością 2 na samym początku, a tym samym przesłonięte zostało wcześniejsze powiązanie go z wartością 10. W rezultacie po wartościowaniu pojawia się nieoczekiwany wynik.

Musimy więc znaleźć sposób na tworzenie symboli, których nazwy byłyby różne od nazw symboli potencjalnie przekazywanych do makr. Taką pewność daje funkcja gensym, która stwarza symbole o niepowtarzalnych nazwach:

 1(defmacro dodaj-dwa [x]
 2  (let [s (gensym 'a)]
 3    `(let [~s 2]
 4       (+ ~s ~x))))
 5
 6(dodaj-dwa (inc 1))
 7; => 4
 8
 9(let [a 10] (dodaj-dwa a))
10; => 12
(defmacro dodaj-dwa [x] (let [s (gensym &#39;a)] `(let [~s 2] (+ ~s ~x)))) (dodaj-dwa (inc 1)) ; =&gt; 4 (let [a 10] (dodaj-dwa a)) ; =&gt; 12

Udało się, jednak w ciele makra musieliśmy wprowadzić dodatkowe powiązanie leksykalne (s), aby pamiętać stworzoną nazwę symbolu. Wywoływanie gensym za każdym razem sprawiłoby, że w każdym miejscu nazwy byłyby inne.

Z pomocą przychodzi tu makro czytnika służące go generowania symboli o unikatowych nazwach:

Przykład generowania symboli w cytowaniu składniowym
1(defmacro dodaj-dwa [x]
2  `(let [a# 2]
3     (+ a# ~x)))
4
5(dodaj-dwa (inc 1))
6; => 4
7
8(let [a 10] (dodaj-dwa a))
9; => 12
(defmacro dodaj-dwa [x] `(let [a# 2] (+ a# ~x))) (dodaj-dwa (inc 1)) ; =&gt; 4 (let [a 10] (dodaj-dwa a)) ; =&gt; 12

Pierwsze użycie generatora symboli wewnętrznie skorzysta z funkcji gensym, aby stworzyć niepowtarzalną nazwę symbolu, która zastąpi zapis jego wywołania. Każde kolejne zastosowanie tego makra czytnika w odniesieniu do symbolicznej nazwy, wobec której już wcześniej zostało użyte, sprawi, że podstawiona zostanie wcześniej wytworzona nazwa.

Spójrzmy jeszcze, jak będzie wyglądało rozwinięcie ostatniej wersji makra wraz z kontekstem jego wywołania:

1(let [a 10]
2  (let* [a__10986__auto__ 2]
3    (clojure.core/+ a__10986__auto__ a)))
(let [a 10] (let* [a__10986__auto__ 2] (clojure.core/+ a__10986__auto__ a)))

Widzimy, że zapis a# został we wszystkich miejscach zamieniony w a__10986__auto__ i nie dochodzi już do konfliktu symbolicznych nazw w powiązaniach leksykalnych.

Diagnozowanie makr

Diagnostyka makr jest potrzebna, ponieważ w metaprogramowaniu bazującym na przekształceniach syntaktycznych z wykorzystaniem mechanizmów szablonowych (interpolacja, cytowanie, transpozycja) łatwo o przeoczenia – struktura danych reprezentująca kod źródłowy bywa często składniowo przemieszana z wyrażeniami, które będą wartościowane.

Rozwijanie makra, macroexpand-1

Funkcja macroexpand-1 służy do generowania rozwinięć makr.

Użycie:

  • (macroexpand-1 konstrukcja).

Jeżeli podana konstrukcja, wyrażona strukturą danych (np. zacytowanym S-wyrażeniem lub zbudowana w inny sposób) jest makrem, dokonane zostanie jego rozwinięcie, a wartością zwracaną będzie struktura danych zawierająca reprezentację S-wyrażenia.

Jeżeli podana konstrukcja nie jest makrem, zwrócona będzie jej własna wartość.

Przykłady użycia funkcji macroexpand-1
 1(defmacro razy       [& args] `(* ~@args))
 2(defmacro razy-2     [& args] `(razy 2 ~@args))
 3(defmacro razy-lista [x]      `(list 2 (razy-2 ~x)))
 4
 5(razy-lista 2)
 6; => (2 4)
 7
 8(macroexpand-1 '(razy-2 2))
 9; => (user/razy 2 2)
10
11(macroexpand-1 '(razy-lista 2))
12; => (clojure.core/list 2 (user/razy-2 2))
(defmacro razy [&amp; args] `(* ~@args)) (defmacro razy-2 [&amp; args] `(razy 2 ~@args)) (defmacro razy-lista [x] `(list 2 (razy-2 ~x))) (razy-lista 2) ; =&gt; (2 4) (macroexpand-1 &#39;(razy-2 2)) ; =&gt; (user/razy 2 2) (macroexpand-1 &#39;(razy-lista 2)) ; =&gt; (clojure.core/list 2 (user/razy-2 2))

Rozwijanie makr, macroexpand

Funkcja macroexpand działa podobnie do macroexpand-1, ale jeżeli rezultatem rozwinięcia podanej konstrukcji jest znowu makro, powtarza operację, do momentu, aż uzyskaną wartością nie będzie makro.

Użycie:

  • (macroexpand konstrukcja).

Funkcja wielokrotnie wywołuje macroexpand-1 na podanej konstrukcji, dopóki zwracana wartość jest makrem. W takim przypadku wywoływanie jest przerywane, a uzyskana wartość zwracana.

Przykład użycia funkcji macroexpand
 1(defmacro razy       [& args] `(* ~@args))
 2(defmacro razy-2     [& args] `(razy 2 ~@args))
 3(defmacro razy-lista [x]      `(list 2 (razy-2 ~x)))
 4
 5(razy-lista 2)
 6; => (2 4)
 7
 8(macroexpand '(razy-2 2))
 9; => (clojure.core/* 2 2)
10
11(macroexpand '(razy-lista 2))
12; => (clojure.core/list 2 (user/razy-2 2))
(defmacro razy [&amp; args] `(* ~@args)) (defmacro razy-2 [&amp; args] `(razy 2 ~@args)) (defmacro razy-lista [x] `(list 2 (razy-2 ~x))) (razy-lista 2) ; =&gt; (2 4) (macroexpand &#39;(razy-2 2)) ; =&gt; (clojure.core/* 2 2) (macroexpand &#39;(razy-lista 2)) ; =&gt; (clojure.core/list 2 (user/razy-2 2))

Rekurencyjne rozwijanie, macroexpand-all

Funkcja macroexpand-all z przestrzeni nazw clojure.walk działa podobnie do macroexpand, ale dokonuje rekursywnego rozwinięcia wszelkich makr zagnieżdżonych w podanej konstrukcji.

Użycie:

  • (clojure.walk/macroexpand-all konstrukcja).

Funkcja wielokrotnie wywołuje macroexpand-1 na podanej konstrukcji (wyrażonej strukturą danych), a także na wszystkich zagnieżdżonych konstrukcjach, dopóki zwracane wartości są makrami.

Wartością zwracaną jest struktura danych reprezentująca S-wyrażenie, które jest podaną jako argument konstrukcją z rekursywnie rozwiniętymi wszystkimi zawartymi makrami.

Przykład użycia funkcji clojure.walk/macroexpand-all
 1(require 'clojure.walk)
 2
 3(defmacro razy       [& args] `(* ~@args))
 4(defmacro razy-2     [& args] `(razy 2 ~@args))
 5(defmacro razy-lista [x]      `(list 2 (razy-2 ~x)))
 6
 7(razy-lista 2)
 8; => (2 4)
 9
10(clojure.walk/macroexpand-all '(razy-2 2))
11; => (clojure.core/* 2 2)
12
13(clojure.walk/macroexpand-all '(razy-lista 2))
14; => (clojure.core/list 2 (clojure.core/* 2 2))
(require &#39;clojure.walk) (defmacro razy [&amp; args] `(* ~@args)) (defmacro razy-2 [&amp; args] `(razy 2 ~@args)) (defmacro razy-lista [x] `(list 2 (razy-2 ~x))) (razy-lista 2) ; =&gt; (2 4) (clojure.walk/macroexpand-all &#39;(razy-2 2)) ; =&gt; (clojure.core/* 2 2) (clojure.walk/macroexpand-all &#39;(razy-lista 2)) ; =&gt; (clojure.core/list 2 (clojure.core/* 2 2))

Przykład implementacji pętli

Spróbujmy użyć wiedzy, którą mamy o makrach do stworzenia implementacji prostej pętli. Powinna ona przyjmować liczbę całkowitą oraz zero lub więcej wyrażeń, a zwracać kod, który powtórzy ewaluację ich wszystkich tyle razy, ile wynosi podana wcześniej wartość. Wartością zwracaną w wątku uruchomieniowym powinna być wartość wyrażenia obliczonego w ostatnim przebiegu pętli.

 1(defmacro powtarzaj
 2  "Powtarza wywołanie podanych wyrażeń określoną liczbę razy.
 3  Zwraca rezultat ostatniego wywołania ostatniego wyrażenia."
 4  [powtórzenia & wyrażenia]
 5  `(let [p# ~powtórzenia]
 6     (loop [x# 0 l# nil]
 7       (if (< x# p#)
 8         (recur (inc x#) (do ~@wyrażenia))
 9         l#))))
10
11(powtarzaj
12 3
13 (println "wow")
14 (+ 2 2))
15
16; >> wow
17; >> wow
18; >> wow
19; => 4
(defmacro powtarzaj &#34;Powtarza wywołanie podanych wyrażeń określoną liczbę razy. Zwraca rezultat ostatniego wywołania ostatniego wyrażenia.&#34; [powtórzenia &amp; wyrażenia] `(let [p# ~powtórzenia] (loop [x# 0 l# nil] (if (&lt; x# p#) (recur (inc x#) (do ~@wyrażenia)) l#)))) (powtarzaj 3 (println &#34;wow&#34;) (+ 2 2)) ; &gt;&gt; wow ; &gt;&gt; wow ; &gt;&gt; wow ; =&gt; 4

W wektorze parametrycznym makra określamy, że przyjmuje ono jeden obowiązkowy argument (powtórzenia) i zero lub więcej argumentów opcjonalnych, które zostaną zgrupowane w wartości parametru wariadycznego wyrażenia.

Od linii nr 5 do końca ciała makra mamy zwracaną przez nie strukturę danych. Będzie to składniowo zacytowana lista. Jej pierwszym elementem jest odwołanie do formy specjalnej let, a drugim wektor powiązań. Dokonujemy w nim wygenerowania symbolu o unikatowej nazwie (p#), który zostanie powiązany z wartością powtórzeń (powtórzenia). Aby ją uzyskać, dokonujemy transpozycji z użyciem znacznika ~.

Dlaczego pobieramy wartość powtórzeń i dokonujemy jej leksykalnego powiązania z symbolem? Argumenty makr nie są przekazywane przez wartość, więc wartością parametru powtórzenia będzie forma reprezentująca S-wyrażenie. Dopóki będzie to wartość stała (np. literał liczbowy), nic złego się nie stanie. Jeżeli jednak jako argument przekażemy zapis (+ 1 2 3), to na etapie ewaluacji każdorazowe odwołanie się do wartości parametru powtórzenia będzie powodowało przeliczanie wyrażenia. Gdy przypadkiem wyrażenie generuje efekty uboczne lub wykonuje bardziej czasochłonne obliczenia możemy mieć kłopoty, jeżeli np. będziemy wielokrotnie wartościowali je w pętli. Z tego względu dokonujemy obliczenia wartości tylko raz, a rezultat zapamiętujemy przez powiązanie z nim symbolu.

Kolejna linia (nr 6) to zagnieżdżone S-wyrażenie, które jest wywołaniem formy specjalnej loop. Dzięki niej możemy powiązać pewne symbole z wartościami początkowymi: pełniący funkcję licznika symbol x# z wartością 0, a symbol l# z wartością ostatnio zwracanego wyrażenia (początkowo nil).

(Dla czytelności używamy tu sformułowań symbol x# czy symbol l#, mając na myśli symbole o unikatowych nazwach, które powstaną w rezultacie przetworzenia wspomnianych wywołań makr czytnika).

W linii nr 7 mamy formę specjalną if, która sprawdza czy spełniony jest warunek zakończenia rekurencji (czy licznik x# powiązany jest z wartością taką, jak wartość powiązana z symbolem p#, czyli czy osiągnięto już właściwą liczbę powtórzeń). Jeżeli jeszcze tak nie jest, wywoływana jest konstrukcja specjalna recur. Dokonuje ona skoku do określonego przez loop miejsca, a dodatkowo zmiany powiązań symboli: x# zostanie powiązany z wartością o 1 większą niż poprzednia, a l# z wartością ostatnio realizowanego wyrażenia.

Warto zauważyć, że podane wyrażenia, które będą wywoływane w pętli, zostaną wydobyte ze struktury, w której zostały przekazane i umieszczone wewnątrz listy rozpoczynającej się symbolem do. W ten sposób grupujemy je w jeden zestaw, którego wartością zwracaną będzie rezultat wartościowania ostatniego umieszczonego wyrażenia.

W ostatniej linii przykładu mamy zwracanie wartości powiązanej z symbolem #l, czyli ostatnio zapamiętanej wartości przeliczanego wyrażenia. Jest to przypadek bazowy rekurencji, po którym pętla kończy pracę.

Spójrzmy jeszcze na rezultat rozwinięcia naszego makra:

Rezultat wywołania macroexpand
 1(macroexpand
 2 '(powtarzaj
 3   3
 4   (println "wow")
 5   (+ 2 2)))
 6
 7; => (let*
 8; =>  [p__10611__auto__ 3]
 9; =>  (clojure.core/loop
10; =>   [x__10612__auto__ 0 l__10613__auto__ nil]
11; =>   (if
12; =>    (clojure.core/< x__10612__auto__ p__10611__auto__)
13; =>    (recur (clojure.core/inc x__10612__auto__) (do (println "wow") (+ 2 2)))
14; =>    l__10613__auto__)))
(macroexpand &#39;(powtarzaj 3 (println &#34;wow&#34;) (+ 2 2))) ; =&gt; (let* ; =&gt; [p__10611__auto__ 3] ; =&gt; (clojure.core/loop ; =&gt; [x__10612__auto__ 0 l__10613__auto__ nil] ; =&gt; (if ; =&gt; (clojure.core/&lt; x__10612__auto__ p__10611__auto__) ; =&gt; (recur (clojure.core/inc x__10612__auto__) (do (println &#34;wow&#34;) (+ 2 2))) ; =&gt; l__10613__auto__)))

Możemy dla czytelności wyrazić go w bardziej konwencjonalnej postaci i wartościować:

 1(let [p 3]
 2  (loop [x 0 l nil]
 3    (if (< x p)
 4      (recur (inc x) (do (println "wow") (+ 2 2)))
 5      l)))
 6
 7; >> wow
 8; >> wow
 9; >> wow
10; => 4
(let [p 3] (loop [x 0 l nil] (if (&lt; x p) (recur (inc x) (do (println &#34;wow&#34;) (+ 2 2))) l))) ; &gt;&gt; wow ; &gt;&gt; wow ; &gt;&gt; wow ; =&gt; 4

Makra wbudowane

Niektóre konstrukcje języka Clojure, które w innych dialektach Lispu są formami specjalnymi, zaś w wielu językach programowania wbudowanymi instrukcjami, zostały zaimplementowane jako makra.

Wyrażenia warunkowe

  • assert – warunkowe generowanie wyjątków;
  • and – suma logiczna;
  • case – obsługa przypadków warunkowych;
  • cond – obsługa wyrażeń warunkowych;
  • condp – zaawansowana obsługa wyrażeń warunkowych;
  • if-not – odwrotna konstrukcja warunkowa;
  • or – alternatywa logiczna;
  • when – wykonywanie warunkowe bez alternatywy;
  • when-not – odwrotne wykonywanie warunkowe bez alternatywy;
  • when-some – wykonywanie warunkowe wartościowych bez alternatywy.

Sterowanie wartościowaniem

  • comment – tworzenie komentarzy;
  • doto – przyłączanie argumentu;
  • -> – przewlekanie S-wyrażeń;
  • ->> – przewlekanie S-wyrażeń od końca;
  • cond-> – warunkowe przewlekanie S-wyrażeń;
  • cond->> – warunkowe przewlekanie S-wyrażeń od końca;
  • some-> – przewlekanie wartościowych S-wyrażeń;
  • some->> – przewlekanie wartościowych S-wyrażeń od końca.

Dostęp do obiektów Javy

  • .. – dostęp do zagnieżdżonych składowych klas Javy.

Funkcje i multimetody

  • fn – definiowanie funkcji;
  • defmulti – definiowanie multimetody z funkcją dyspozycyjną;
  • defmethod – definiowanie metody przypisywanej do multimetody;
  • memfn – tworzenie funkcji wywołujących metody Javy;
  • defn- – definiowanie funkcji prywatnych.

Makra

Kolekcje i sekwencje

  • amap – odwzorowywanie tablic Javy;
  • areduce – skracanie tablic Javy;
  • defstruct – definiowanie map strukturalizowanych;
  • doseq – wymuszanie wartościowania leniwych sekwencji;
  • for – iterowanie po elementach sekwencji;
  • lazy-cat – złączanie leniwych sekwencji;
  • lazy-seq – generowanie leniwych sekwencji;
  • pvalues – współbieżne generowanie leniwej sekwencji z wartości wyrażeń.

Powiązania leksykalne

  • if-let – powiązania warunkowe;
  • if-some – warunkowe powiązania wartościowych;
  • let – powiązania leksykalne;
  • letfn – powiązania leksykalne funkcji;
  • when-first – powiązania niepustego elementu sekwencji;
  • when-let – powiązania z warunkiem;
  • when-some – powiązania wartościowych.

Zmienne globalne i typ Var

  • binding – przesłanianie zmiennej dynamicznej;
  • defonce – jednokrotne definiowanie zmiennych globalnych;
  • with-bindings – przesłanianie zmiennych dynamicznych;
  • with-local-vars — obsługa zmiennych lokalnych;
  • with-redefs – obsługa redefinicji zmiennych globalnych.

Przestrzenie nazw

  • declare – deklarowanie zmiennych;
  • import – importowanie bibliotek;
  • ns – ustawianie bieżącej przestrzeni nazw;
  • refer-clojure – dodawanie odniesień do obiektów typu Var;

Pętle i rekurencja

  • dotimes – pętla powtórzeniowa;
  • loop – punkt startowy rekurencji ogonowej;
  • while – pętla warunkowa.

Obsługa łańcuchów znakowych

  • with-in-str – łańcuch znakowy ze standardowego wejścia,
  • with-out-str – łańcuch znakowy ze standardowego wyjścia.

Obsługa typów numerycznych

Obsługa wejścia/wyjścia

  • with-open – operowanie na wejściu/wyjściu.

Wykonywanie współbieżne

  • bound-fn – zachowywanie wątkowych powiązań funkcji;
  • delay – tworzenie obiektów typu Delay;
  • dosync – tworzenie transakcji;
  • future – tworzenie obiektów typu Future;
  • io! – oznaczanie funkcji obsługujących wejście/wyjście;
  • locking – zakładanie blokad na obiekty;
  • time – obliczanie czasu wykonywania wyrażeń;
  • vswap! – podmiana wartości bieżącej obiektów typu Volatile.
Jesteś w sekcji Poczytaj mi Clojure
Tematyka:

Taksonomie: