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 <stdio.h>
#define non_zero(x) ((x) > 0)
int main(int args, char *argv[]) {
int a = 5;
if (non_zero(a)) {
printf("a jest większe od zera\n");
}
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:
- analiza leksykalna,
- analiza składniowa,
- analiza semantyczna,
- generowanie i optymalizacja kodu wynikowego,
- konsolidacja kodu wynikowego,
- 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):
-
- Wczytanie tekstu kodu źródłowego (analiza leksykalna).
- Zmiana S-wyrażeń w obiekty drzewa składniowego (analiza składniowa).
- Rozwijanie makr i aktualizacja drzewa składniowego.
-
- Rozpoznawanie form (analiza semantyczna).
- Generowanie kodu bajtowego.
- 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:
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) (< 1 2) "prawda") ; => "prawda"
(if-let [x 5] x) ; => 5
(dotimes [x 2] (println x)) ; => nil >> 0 1
(doto 1 println) ; => 1 >> 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:
- lista będzie reprezentowała listowe S-wyrażenie,
- mapa będzie reprezentowała mapowe S-wyrażenie,
- wektor będzie reprezentował wektorowe S-wyrażenie,
- zbiór będzie reprezentował zbiorowe S-wyrażenie.
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:
nazwa
– nazwa makra i reprezentującej go zmiennej globalnej (Symbol
),dok
– łańcuch dokumentujący (String
),metadane
– metadane zmiennej globalnej (mapowe S-wyrażenie),parametry
– wektor parametryczny (wektorowe S-wyrażenie),wyrażenie
– ciało makra składające się z wyrażeń.
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.
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
"Zwraca tekstową reprezentację podanego wyrażenia."
[wyrażenie]
(str "WYWOŁANO: " wyrażenie))
(do-tekstu (+ 1 2 3)) ; => "WYWOŁANO: (+ 1 2 3)"
(do-tekstu (+ 1, 2, 3)) ; => "WYWOŁANO: (+ 1 2 3)"
(do-tekstu "test") ; => "WYWOŁANO: test"
(defmacro odwróć-argumenty
"Odwraca argumenty podanych wyrażeń."
[& wyrażenia]
(cons 'do (map #(cons (first %1) (reverse (rest %1))) wyrażenia)))
(odwróć-argumenty
(println 1 2 3)
(vector 1 2 3))
; >> 3 2 1
; => [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:
- „Zmienne globalne”, rozdział VII,
- „Funkcje i domknięcia”, rozdział VIII.
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ą:
- wyłączać cytowanie (wymuszać wartościowanie),
- wyłączać cytowanie z wydobywaniem elementów (dla kolekcji),
- generować symbole o unikatowych nazwach.
Cytowanie składniowe działa dla S-wyrażeń, które w pamięci reprezentowane są z użyciem symboli, list, wektorów, zbiorów i map. 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ę.
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) ; => (1 2 3)
`(vector a b) ; => (clojure.core/vector user/a user/b)
`(vector 1 2 3 [a b]) ; => (clojure.core/vector 1 2 3 [user/a user/b])
`[a b 1 2 3] ; => [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ń:
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)) #"\.")))
(if (coll? wyr) (map jaki-typ wyr) wyr)))
(jaki-typ `(1 2 3))
; => (Cons ; komórki Cons
; => ((Long 1) ; liczba całkowita
; => (Long 2) ; liczba całkowita
; => (Long 3))) ; liczba całkowita
(jaki-typ `(vector a b))
; => (Cons ; komórki Cons
; => ((Symbol clojure.core/vector) ; symbol
; => (Symbol user/a) ; symbol
; => (Symbol user/b))) ; symbol
(jaki-typ `(vector 1 2 3 [a b]))
; => (Cons ; komórki Cons
; => ((Symbol clojure.core/vector) ; symbol
; => (Long 1) ; liczba całkowita
; => (Long 2) ; liczba całkowita
; => (Long 3) ; liczba całkowita
; => (PersistentVector ; wektor
; => ((Symbol user/a) ; symbol
; => (Symbol user/b))))) ; symbol
(jaki-typ `[a b 1 2 3])
; => (PersistentVector ; wektor
; => ((Symbol user/a) ; symbol
; => (Symbol user/b) ; symbol
; => (Long 1) ; liczba całkowita
; => (Long 2) ; liczba całkowita
; => (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.
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])
; => (clojure.core/vector 1 2 3 [:a :b :c])
;; transponujemy całe listowe S-wyrażenie:
`~(list a b c)
; => (:a :b :c)
;; transponujemy dwa pierwsze symbole wektorowego S-wyrażenia:
`[~a ~b c 1 2 3]
; => [: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)
; => 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.
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])
; => (clojure.core/vector 1 2 3 :a :b :c)
`(~@(list a b c))
; => (:a :b :c)
`(a b c ~@[1 2 3])
; => (user/a user/b user/c 1 2 3)
`(1 2 3 ~@(list (+ 2 2)))
; => (1 2 3 4)
(defmacro sumuj [& args] `(+ ~@args))
(sumuj 1 2 3 4)
; => 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:
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
"Oblicza wartości parzystych wyrażeń podanych jako argumenty
i zwraca wartość ostatniego."
[& wyrażenia]
`(do
~@(take-nth 2 (rest wyrażenia))))
(rób-parzyste
(println "pierwsze")
(println "drugie")
(+ 2 2)
"koniuszek")
; >> drugie
; => "koniuszek"
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
:
`(let [a 2] a)
; => (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)
; >> java.lang.RuntimeException: Can'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:
(defmacro pisz-dwa []
`(let [~(symbol "a") 2]
~(symbol "a")))
(pisz-dwa)
; => 2
Inna metoda to użycie transponowania w odniesieniu do zacytowanych symboli:
(defmacro pisz-dwa []
`(let [~'a 2]
~'a))
(pisz-dwa)
; => 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 [~'a 2]
(+ ~'a ~x)))
(dodaj-dwa (inc 1))
; => 4
(let [a 10] (dodaj-dwa a))
; => 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:
(let* [a 2]
(clojure.core/+ a a))
Uwzględniając leksykalne otoczenie, będziemy mieli do czynienia z następującym kodem:
(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 'a)]
`(let [~s 2]
(+ ~s ~x))))
(dodaj-dwa (inc 1))
; => 4
(let [a 10] (dodaj-dwa a))
; => 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:
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))
; => 4
(let [a 10] (dodaj-dwa a))
; => 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:
(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ść.
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 [& args] `(* ~@args))
(defmacro razy-2 [& args] `(razy 2 ~@args))
(defmacro razy-lista [x] `(list 2 (razy-2 ~x)))
(razy-lista 2)
; => (2 4)
(macroexpand-1 '(razy-2 2))
; => (user/razy 2 2)
(macroexpand-1 '(razy-lista 2))
; => (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.
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 [& args] `(* ~@args))
(defmacro razy-2 [& args] `(razy 2 ~@args))
(defmacro razy-lista [x] `(list 2 (razy-2 ~x)))
(razy-lista 2)
; => (2 4)
(macroexpand '(razy-2 2))
; => (clojure.core/* 2 2)
(macroexpand '(razy-lista 2))
; => (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.
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 'clojure.walk)
(defmacro razy [& args] `(* ~@args))
(defmacro razy-2 [& args] `(razy 2 ~@args))
(defmacro razy-lista [x] `(list 2 (razy-2 ~x)))
(razy-lista 2)
; => (2 4)
(clojure.walk/macroexpand-all '(razy-2 2))
; => (clojure.core/* 2 2)
(clojure.walk/macroexpand-all '(razy-lista 2))
; => (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
"Powtarza wywołanie podanych wyrażeń określoną liczbę razy.
Zwraca rezultat ostatniego wywołania ostatniego wyrażenia."
[powtórzenia & wyrażenia]
`(let [p# ~powtórzenia]
(loop [x# 0 l# nil]
(if (< x# p#)
(recur (inc x#) (do ~@wyrażenia))
l#))))
(powtarzaj
3
(println "wow")
(+ 2 2))
; >> wow
; >> wow
; >> wow
; => 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:
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
'(powtarzaj
3
(println "wow")
(+ 2 2)))
; => (let*
; => [p__10611__auto__ 3]
; => (clojure.core/loop
; => [x__10612__auto__ 0 l__10613__auto__ nil]
; => (if
; => (clojure.core/< x__10612__auto__ p__10611__auto__)
; => (recur (clojure.core/inc x__10612__auto__) (do (println "wow") (+ 2 2)))
; => 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 (< x p)
(recur (inc x) (do (println "wow") (+ 2 2)))
l)))
; >> wow
; >> wow
; >> wow
; => 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 typuVar
;
Pętle i rekurencja
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
with-precision
– określanie dokładności obliczeń.
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 typuDelay
;dosync
– tworzenie transakcji;future
– tworzenie obiektów typuFuture
;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 typuVolatile
.