Określenie ,,makra'' jest mało adekwatne, bo sugeruje coś w rodzaju preprocesora C lub m4. W dialektach Lispu makra pozwalają definiować całkowicie nowe konstrukcje syntaktyczne. Takie rozszerzenie syntaktyczne (potocznie nazywane makrem) sygnalizowane jest nazwą/symbolem i ma postać podobną do już istniejących, na przykład
(while warunek wyrażenie)które wygląda podobnie do standardowego
(when warunek wyrażenie)
Definicje makr mogą używać pełnej mocy języka, tyle że są to funkcje, które operują na wyrażeniach, a nie na wartościach. Definicja makra związuje nazwę makra z jego funkcją transformacji wyrażeń, zwaną transformerem.
Wynik transformacji zastępuje w programie oryginalne wyrażenie. Ponieważ może zawierać kolejne makrowołania, więc rozwijanie makr jest procesem rekurencyjnym.
Mechanizmy niskopoziomowe definiowania makr nie wchodzą obecnie w skład standardu Scheme (naruszono tu po raz pierwszy w historii tych standardów podstawową zasadę: ,,należy usuwać ograniczenia języka, a nie spiętrzać je''). Występują jednak w każdej implementacji i są wszędzie prawie jednakowe. My oczywiście popatrzymy na Guile.
Do definiowania makr służy konstrukcja
(define-macro (nazwa argument ...) wyrażenie ...)Makro jest normalną definicją funkcji, tyle że:
Popatrzmy na przykład dla while
(define-macro (while test . body) (list* 'do '() (list (list 'not test)) body)) > (let ((i 3)) (while (> i 0) (display i) (newline) (set! i (- i 1))) 3 2 1
Jak sprawdzić, czy wynik rozwinięcia makra jest zgodny z naszymi
oczekiwaniami. Niestety nie jest to łatwe. Istnieje funkcja
macroexpand
, ale pokazuje ona wynik w wewnętrznej postaci,
niebyt wygodnej do oglądania:
scheme@(guile-user)> (macroexpand '(let ((i 3)) (while (> i 0) (display i) (newline) (set! i (- i 1))))) $4 = #<tree-il (let (i) (i-544) ((const 3)) (letrec (loop) (loop-548) ((lambda ((name . loop)) (lambda-case ((() #f #f #f () ()) (if (apply (toplevel not) (apply (toplevel >) (lexical i i-544) (const 0))) (if (const #f) (const #f) (void)) (begin (apply (toplevel display) (lexical i i-544)) (apply (toplevel newline)) (set! (lexical i i-544) (apply (toplevel -) (lexical i i-544) (const 1))) (apply (lexical loop loop-548)))))))) (apply (lexical loop loop-548))))>
Sama makrodefnicja też nie była zbyt czytelna, ale z tym łatwo sobie
poradzić. Scheme posiada mechanizm ,,półcytowania'' (quasiquote),
podobny do quote
. Jest on bardzo przydatny do czytelnego opisu
złożonych wyrażeń budujących struktury listowe (i wektorowe). Używamy
znaku "`" zamiast apostrofu.
Działa to jak zwykły quote
z dwoma wyjątkami:
Popatrzmy teraz na naszą definicję
(define-macro (while test . body) `(do () ((not ,test)) ,@body))Prawda, że dużo czytelniej?
To teraz pora na ćwiczenia
let
w wersji bazowej. Ma
działać tak
(my-let ((var init) ...) body ...) ==> ((lambda (var ...) body ...) init ...)Rozwiązanie tutaj
and
(and) ==> #t (and e) ==> e (and e1 e2 ...) ==> (if e1 (and e2 ...) #f)Rozwiązanie tutaj
Co by było, gdybym zdefiniował while
używając nazwanego
let
(define-macro (while test . body) `(let iter () (when ,test ,@body (iter)))) > (let ((iter 3)) (while (> iter 0) (display iter) (newline) (set! iter (- iter 1)))) <unnamed port>:65:23: In procedure iter: <unnamed port>:65:23: In procedure >: Wrong type argument in position 1: #
Popatrzmy na definicję makra or
(define-macro (my-or . args) (cond ((null? args) #f) ((null? (cdr args)) (car args)) (else `((lambda (temp) (if temp temp (my-or ,@(cdr args)))) ,(car args))))) scheme@(guile-user)> (my-or #f 2 3) $6 = 2
Wygląda na to, że jest dobrze. Ale próbujmy dalej
scheme@(guile-user)> (let ((temp 7)) (my-or #f temp 3)) $7 = 3 scheme@(guile-user)> (let ((temp1 7)) (my-or #f temp1 3)) $8 = 7Ups... Chyba jednak nie jest tak świetnie - zmiana nazwy zmiennej lokalnej zmieniła wynik.
Co się stało? Identyfikator temp
został przechwycony:
utożsamiony ze zmienną lokalną wprowadzoną w treści makra. Naruszyliśmy
przejrzystość referencyjną.
Popatrzmy na inny przykład z (częściowym) rozwinięciem
(let ((temp 7)) (my-or (foo temp) temp)) ==> (let ((temp 7)) ((lambda (temp) (if temp temp temp)) (foo temp)))
Można by zmienić w treści makra tę nazwę na inną, na
przykład %temp@^&%-
. Takie dziwaczne nazwy generuje funkcja
gensym
(zaczynają się od spacji), ale ryzyko wciąż istnieje.
Można jednak dostać unikalny symbol: taki, który nie jest ,,internowany'' --\umieszczany w bazie symboli. Nie można go jednak wczytać ani wypisać.
Tworzymy go funkcją make-symbol
(define-macro (my-or . args) (cond ((null? args) #f) ((null? (cdr args)) (car args)) (else (let ((temp (make-symbol "temp"))) `((lambda (,temp) (if ,temp ,temp (my-or ,@(cdr args)))) ,(car args)))))) scheme@(guile-user)> (let ((temp1 7)) (my-or #f temp1 3)) $9 = 7 scheme@(guile-user)> (let ((temp 7)) (my-or #f temp 3)) $10 = 7
Inny sposób to użyć zero-argumentowych procedur (thunk)
(define-macro (my-or . args) (cond ((null? args) #f) ((null? (cdr args)) (car args)) (else `((lambda (temp thunk) (if temp temp (thunk))) ,(car args) (lambda () (my-or ,@(cdr args)))))))
Bardzo eleganckie teoretycznie, ale ciut skomplikowane dla większych definicji. Łatwo popełnić błąd.
Wróćmy jeszcze do przykładu z while
używającym nazwanego
let
(define-macro (while test . body) (let ((iter (make-symbol "iter"))) `(let ,iter () (when ,test ,@body (,iter))))) > (let ((iter 3)) (while (> iter 0) (display iter) (newline) (set! iter (- iter 1)))) 3 2 1
syntax-rules
w definicjach niskopoziomowych możemy zrobić wszystko -- do dyspozycji mamy całą moc Scheme. Ale uwaga: możemy też popsuć wszystko. Dlatego w standardzie R5RS do Scheme dołożono definiowanie konstrukcji syntaktycznych opisując je parami (wzorzec, rozwinięcie). Oddajemy trochę mocy, mamy wygodę i bezpieczeństwo. Popatrzmy na znane już definicje
(define-syntax while (syntax-rules () ((_ test body ...) (do () ((not test)) body ...)))) (define-syntax my-let (syntax-rules () ((let ((name val) ...) body ...) ((lambda (name ...) body ...) val ...)))
Pierwszy argument syntax-rules
to lista symboli
zarezerwowanych w definicji -- system traktuje je jako stałe.
Zwykle ta lista jest pusta (nazwa makra jest zarezerwowana implicite).
Pozostałe symbole pełnią rolę zmiennych syntaktycznych.
Reszta to reguły: pary wzorzec + rozwinięcie. Transformer
znajduje pierwszą regułę, której wzorzec pasuje do wyrażenia i
zastępuje wyrażenie nowym, utworzonym z rozwinięcia. Symbol ...
oznacza wielokrotne potórzenie poprzedzającego elementu.
To teraz or
(define-syntax (or) (syntax-rules () ((or) #f) ((or e) e) ((or e1 e2 ...) ((lambda (temp) (if temp temp (or e2 ...))) e1)))
I kilka innych przykładów
(define-syntax while (syntax-rules () ((while test body ...) (let iter () (when test body ... (iter))))))
Pora poćwiczyć:
(until test expr ...)
oblicza cyklicznie wyrażenia dopóki test nie będzie spełniony.
Rozwiązanie tutaj
let*
działa jak let
, ale tworzy
wiązania po kolei (zagnieżdża).
Rozwiązanie tutaj
Makro define-stub służy do definiowania funcji ,,zastępczych'', zamiast właściwej funkcji,