common-lisp
makra
Szukaj…
Uwagi
Cel makr
Makra są przeznaczone do generowania kodu, przekształcania kodu i dostarczania nowych notacji. Te nowe notacje mogą lepiej pasować do lepszego wyrażania programu, na przykład poprzez dostarczanie konstrukcji na poziomie domeny lub całych nowych języków osadzonych.
Makra mogą uczynić kod źródłowy bardziej zrozumiałym, ale debugowanie może być utrudnione. Zasadniczo nie należy używać makr, gdy działa zwykła funkcja. Kiedy ich używasz, unikaj typowych pułapek, staraj się trzymać często używanych wzorców i konwencji nazewnictwa.
Makroekspansja
W porównaniu z funkcjami makra są rozwijane w odwrotnej kolejności; skrajnie pierwszy, skrajnie ostatni. Oznacza to, że domyślnie nie można używać makra wewnętrznego do generowania składni wymaganej dla makra zewnętrznego.
Zamówienie ewaluacyjne
Czasami makra muszą przenosić formularze dostarczone przez użytkownika. Należy upewnić się, aby nie zmieniać kolejności, w jakiej są oceniane. Użytkownik może polegać na działaniach niepożądanych występujących w kolejności.
Oceń tylko raz
Rozszerzenie makra często musi wykorzystywać wartość tego samego formularza dostarczonego przez użytkownika więcej niż jeden raz. Możliwe, że forma ma skutki uboczne lub może wywoływać kosztowną funkcję. Dlatego makro musi się upewnić, że takie formularze są oceniane tylko raz. Zwykle odbywa się to poprzez przypisanie wartości do zmiennej lokalnej (której nazwa to GENSYM
ed).
Funkcje używane przez makra przy użyciu EVAL-WHEN
Złożone makra często mają zaimplementowane części logiki w osobnych funkcjach. Należy jednak pamiętać, że makra są rozwijane przed skompilowaniem faktycznego kodu. Podczas kompilowania pliku funkcje i zmienne zdefiniowane w tym samym pliku domyślnie nie będą dostępne podczas wykonywania makra. Wszystkie definicje funkcji i zmiennych w tym samym pliku, używane przez makro, muszą być zawarte w formularzu EVAL-WHEN
. EVAL-WHEN
powinien mieć wszystkie trzy razy określone, kiedy załączony kod powinien być również oceniany podczas ładowania i środowiska wykonawczego.
(eval-when (:compile-toplevel :load-toplevel :execute)
(defun foobar () ...))
Nie dotyczy to funkcji wywoływanych z rozwinięcia makra, tylko funkcje wywoływane przez samo makro.
Typowe wzory makro
DO ZROBIENIA: Może przenieś objaśnienia do uwag i dodaj przykłady osobno
FOOF
W Common Lisp istnieje koncepcja ogólnych odniesień . Pozwalają programistom ustawiać wartości w różnych „miejscach” tak, jakby były zmiennymi. Makra korzystające z tej umiejętności często mają w nazwie poprawkę F
Miejsce jest zazwyczaj pierwszym argumentem makra.
Przykłady ze standardu: INCF
, DECF
, ROTATEF
, SHIFTF
, REMF
.
Głupi przykład, makro, które zamienia znak sklepu liczbowego w miejscu:
(defmacro flipf (place)
`(setf ,place (- ,place)))
Z-FOO
Makra, które nabywają i bezpiecznie zwolnić zasób są zwykle nazywane z WITH-
-prefix. Makro powinno zwykle używać składni, takiej jak:
(with-foo (variable details-of-the-foo...)
body...)
Przykłady ze standardu: WITH-OPEN-FILE
, WITH-OPEN-STREAM
, WITH-INPUT-FROM-STRING
, WITH-OUTPUT-TO-STRING
.
Jednym z podejść do wdrożenia tego typu makr, które pozwala uniknąć niektórych pułapek zanieczyszczenia nazwy i niezamierzonej wielokrotnej oceny, jest wdrożenie najpierw wersji funkcjonalnej. Na przykład pierwszym krokiem w implementacji makra with-widget
który bezpiecznie tworzy widżet i czyści go później, może być funkcja:
(defun call-with-widget (args function)
(let ((widget (apply #'make-widget args))) ; obtain WIDGET
(unwind-protect (funcall function widget) ; call FUNCTION with WIDGET
(cleanup widget) ; cleanup
Ponieważ jest to funkcja, nie ma obaw o zakres nazw w ramach funkcji lub dostawcy i ułatwia napisanie odpowiedniego makra:
(defmacro with-widget ((var &rest args) &body body)
`(call-with-widget (list ,@args) (lambda (,var) ,@body)))
DO-FOO
Makra iterujące się nad czymś często są nazywane z przedrostkiem DO
. Składnia makr powinna zwykle mieć formę
(do-foo (variable the-foo-being-done return-value)
body...)
Przykłady ze standardu: DOTIMES
, DOLIST
, DO-SYMBOLS
.
FOOCASE, EFOOCASE, CFOOCASE
Makra pasujące do danych wejściowych w niektórych przypadkach są często nazywane z poprawką CASE
. Często występuje zmienna E...CASE
, która sygnalizuje błąd, jeśli dane wejściowe nie pasują do żadnego z przypadków, i C...CASE
, która sygnalizuje błąd ciągły. Powinny mieć podobną składnię
(foocase input
(case-to-match-against (optionally-some-params-for-the-case)
case-body-forms...)
more-cases...
[(otherwise otherwise-body)])
Przykłady ze standardu: CASE
, TYPECASE
, HANDLER-CASE
.
Na przykład makro, które dopasowuje ciąg znaków do wyrażeń regularnych i wiąże grupy rejestrów ze zmiennymi. Używa CL-PPCRE do wyrażeń regularnych.
(defmacro regexcase (input &body cases)
(let ((block-sym (gensym "block"))
(input-sym (gensym "input")))
`(let ((,input-sym ,input))
(block ,block-sym
,@(loop for (regex vars . body) in cases
if (eql regex 'otherwise)
collect `(return-from ,block-sym (progn ,vars ,@body))
else
collect `(cl-ppcre:register-groups-bind ,vars
(,regex ,input-sym)
(return-from ,block-sym
(progn ,@body))))))))
(defun test (input)
(regexcase input
("(\\d+)-(\\d+)" (foo bar)
(format t "Foo: ~a, Bar: ~a~%" foo bar))
("Foo: (\\w+)$" (foo)
(format t "Foo: ~a.~%" foo))
(otherwise (format t "Didn't match.~%"))))
(test "asd 23-234 qwe")
; Foo: 23, Bar: 234
(test "Foo: Foobar")
; Foo: Foobar.
(test "Foo: 43 - 23")
; Didn't match.
DEFINE-FOO, DEFFOO
Makra, które definiują rzeczy, są zwykle nazywane albo z DEFINE-
lub DEF
-prefiksem.
Przykłady ze standardu: DEFUN
, DEFMACRO
, DEFINE-CONDITION
.
Makro anaforyczne
Makro anaforyczne to makro, które wprowadza zmienną (często IT
), która przechwytuje wynik formularza dostarczonego przez użytkownika. Typowym przykładem jest Anaphoric If, który jest jak zwykły IF
, ale także definiuje zmienną IT
aby odwoływała się do wyniku formularza testowego.
(defmacro aif (test-form then-form &optional else-form)
`(let ((it ,test-form))
(if it ,then-form ,else-form)))
(defun test (property plist)
(aif (getf plist property)
(format t "The value of ~s is ~a.~%" property it)
(format t "~s wasn't in ~s!~%" property plist)))
(test :a '(:a 10 :b 20 :c 30))
; The value of :A is 10.
(test :d '(:a 10 :b 20 :c 30))
; :D wasn't in (:A 10 :B 20 :C 30)!
MAKROEKSPAND
Rozwijanie makr to proces przekształcania makr w rzeczywisty kod. Zwykle dzieje się to w ramach procesu kompilacji. Kompilator rozszerzy wszystkie formularze makr przed faktyczną kompilacją kodu. Rozwijanie makr odbywa się również podczas interpretacji kodu Lisp.
Można wywołać MACROEXPAND
ręcznie, aby zobaczyć, jak rozwija się forma makra.
CL-USER> (macroexpand '(with-open-file (file "foo")
(do-something-with file)))
(LET ((FILE (OPEN "foo")) (#:G725 T))
(UNWIND-PROTECT
(MULTIPLE-VALUE-PROG1 (PROGN (DO-SOMETHING-WITH FILE)) (SETQ #:G725 NIL))
(WHEN FILE (CLOSE FILE :ABORT #:G725))))
MACROEXPAND-1
jest taki sam, ale rozwija się tylko raz. Jest to przydatne, gdy próbujesz zrozumieć formę makra, która rozwija się do innej formy makra.
CL-USER> (macroexpand-1 '(with-open-file (file "foo")
(do-something-with file)))
(WITH-OPEN-STREAM (FILE (OPEN "foo")) (DO-SOMETHING-WITH FILE))
Zauważ, że ani MACROEXPAND
ani MACROEXPAND-1
rozwijają kodu Lisp na wszystkich poziomach. Rozszerzają jedynie formę makra najwyższego poziomu. Aby w pełni makro rozwinąć formularz na wszystkich poziomach, potrzebna jest do tego chodziarka do kodu . Ta funkcja nie jest dostępna w standardzie Common Lisp.
Backquote - pisanie szablonów kodów dla makr
Kod powrotu makr. Ponieważ kod w Lisp składa się z list, do jego wygenerowania można użyć zwykłych funkcji manipulowania listami.
;; A pointless macro
(defmacro echo (form)
(list 'progn
(list 'format t "Form: ~a~%" (list 'quote form))
form))
Jest to często bardzo trudne do odczytania, szczególnie w dłuższych makrach. Makro czytnika Backquote pozwala pisać szablony cytowane, które są wypełniane przez selektywną ocenę elementów.
(defmacro echo (form)
`(progn
(format t "Form: ~a~%" ',form)
,form))
(macroexpand '(echo (+ 3 4)))
;=> (PROGN (FORMAT T "Form: ~a~%" '(+ 3 4)) (+ 3 4))
Ta wersja wygląda prawie jak zwykły kod. Przecinki są używane do oceny FORM
; wszystko inne jest zwracane tak, jak jest. Zauważ, że w ',form
pojedynczy cytat znajduje się poza przecinkiem, więc zostanie zwrócony.
Można również użyć ,@
aby podzielić listę na pozycji.
(defmacro echo (&rest forms)
`(progn
,@(loop for form in forms collect `(format t "Form: ~a~%" ,form))
,@forms))
(macroexpand '(echo (+ 3 4)
(print "foo")
(random 10)))
;=> (PROGN
; (FORMAT T "Form: ~a~%" (+ 3 4))
; (FORMAT T "Form: ~a~%" (PRINT "foo"))
; (FORMAT T "Form: ~a~%" (RANDOM 10))
; (+ 3 4)
; (PRINT "foo")
; (RANDOM 10))
Backquote można również stosować poza makrami.
Unikalne symbole zapobiegające konfliktom nazw w makrach
Rozwinięcie makra często wymaga użycia symboli, które nie zostały przekazane przez użytkownika jako argumenty (na przykład jako nazwy zmiennych lokalnych). Należy upewnić się, że takie symbole nie mogą kolidować z symbolem, którego użytkownik używa w otaczającym kodzie.
Zwykle osiąga się to za pomocą GENSYM
, funkcji, która zwraca świeży, niezamierzony symbol.
Zły
Rozważ poniższe makro. Tworzy DOTIMES
która również zbiera wynik treści do listy, która jest zwracana na końcu.
(defmacro dotimes+collect ((var count) &body body)
`(let ((result (list)))
(dotimes (,var ,count (nreverse result))
(push (progn ,@body) result))))
(dotimes+collect (i 5)
(format t "~a~%" i)
(* i i))
; 0
; 1
; 2
; 3
; 4
;=> (0 1 4 9 16)
Wydaje się, że działa to w tym przypadku, ale jeśli zdarzy się, że użytkownik ma nazwę zmiennej RESULT
, której używa w ciele, wyniki prawdopodobnie nie będą zgodne z oczekiwaniami użytkownika. Rozważ tę próbę napisania funkcji, która zbiera listę sum wszystkich liczb całkowitych do N
:
(defun sums-upto (n)
(let ((result 0))
(dotimes+collect (i n)
(incf result i))))
(sums-upto 10) ;=> Error!
Dobry
Aby rozwiązać problem, musimy użyć GENSYM
aby wygenerować unikalną nazwę GENSYM
RESULT
w rozwinięciu makra.
(defmacro dotimes+collect ((var count) &body body)
(let ((result-symbol (gensym "RESULT")))
`(let ((,result-symbol (list)))
(dotimes (,var ,count (nreverse ,result-symbol))
(push (progn ,@body) ,result-symbol)))))
(sums-upto 10) ;=> (0 1 3 6 10 15 21 28 36 45)
DO ZROBIENIA: Jak tworzyć symbole z ciągów
DO ZROBIENIA: Unikanie problemów z symbolami w różnych pakietach
jeśli-let, kiedy-let, -let makra
Te makra łączą przepływ kontrolny i wiązanie. Stanowią ulepszenie w stosunku do anaforycznych anaforycznych makr, ponieważ pozwalają programistom komunikować znaczenie poprzez nazewnictwo. Jako takie zaleca się ich stosowanie w porównaniu z anaforycznymi odpowiednikami.
(if-let (user (get-user user-id))
(show-dashboard user)
(redirect 'login-page))
Makra FOO-LET
wiążą jedną lub więcej zmiennych, a następnie używają tych zmiennych jako formularza testowego dla odpowiedniego warunku ( IF
, WHEN
). Wiele zmiennych łączy się z AND
. Wybrana gałąź jest wykonywana z działającymi powiązaniami. Prosta implementacja IF-LET
zmienną może wyglądać mniej więcej tak:
(defmacro if-let ((var test-form) then-form &optional else-form)
`(let ((,var ,test-form))
(if ,var ,then-form ,else-form)))
(macroexpand '(if-let (a (getf '(:a 10 :b 20 :c 30) :a))
(format t "A: ~a~%" a)
(format t "Not found.~%")))
; (LET ((A (GETF '(:A 10 :B 20 :C 30) :A)))
; (IF A
; (FORMAT T "A: ~a~%" A)
; (FORMAT T "Not found.~%")))
Wersja obsługująca wiele zmiennych jest dostępna w bibliotece Aleksandrii .
Używanie makr do definiowania struktur danych
Powszechnym zastosowaniem makr jest tworzenie szablonów dla struktur danych, które przestrzegają wspólnych reguł, ale mogą zawierać różne pola. Pisząc makro, możesz zezwolić na szczegółową konfigurację struktury danych bez konieczności powtarzania kodu wzorcowego, ani na użycie mniej wydajnej struktury (takiej jak skrót) w pamięci wyłącznie w celu uproszczenia programowania.
Załóżmy na przykład, że chcemy zdefiniować szereg klas, które mają szereg różnych właściwości, każda z funkcją pobierającą i ustawiającą. Ponadto, w przypadku niektórych (ale nie wszystkich) tych właściwości, chcemy, aby ustawiający wywoływał metodę na obiekcie, powiadamiając ją o zmianie właściwości. Mimo że Common LISP ma już skrót do pisania programów pobierających i ustawiających, napisanie standardowego niestandardowego narzędzia ustawiającego w ten sposób normalnie wymagałoby duplikacji kodu wywołującego metodę powiadamiania w każdym systemie ustawiającym, co może być uciążliwe, jeśli w grę wchodzi duża liczba właściwości . Jednak definiowanie makra staje się znacznie łatwiejsze:
(defmacro notifier (class slot)
"Defines a setf method in (class) for (slot) which calls the object's changed method."
`(defmethod (setf ,slot) (val (item ,class))
(setf (slot-value item ',slot) val)
(changed item ',slot)))
(defmacro notifiers (class slots)
"Defines setf methods in (class) for all of (slots) which call the object's changed method."
`(progn
,@(loop for s in slots collecting `(notifier ,class ,s))))
(defmacro defclass-notifier-slots (class nslots slots)
"Defines a class with (nslots) giving a list of slots created with notifiers, and (slots) giving a list of slots created with regular accessors."
`(progn
(defclass ,class ()
( ,@(loop for s in nslots collecting `(,s :reader ,s))
,@(loop for s in slots collecting `(,s :accessor ,s))))
(notifiers ,class ,nslots)))
Możemy teraz pisać (defclass-notifier-slots foo (bar baz qux) (waldo))
i natychmiast zdefiniować klasę foo
za pomocą zwykłego slotu waldo
(utworzonego przez drugą część makra ze specyfikacją (waldo :accessor waldo)
) oraz sloty bar
, baz
i qux
z ustawiaczami, które wywołują changed
metodę (gdzie getter jest zdefiniowany w pierwszej części makra, (bar :reader bar)
, a setter przez wywołane makro notifier
).
Oprócz umożliwienia nam szybkiego zdefiniowania wielu klas, które zachowują się w ten sposób, z dużą liczbą właściwości, bez powtórzeń, mamy zwykłą zaletę ponownego użycia kodu: jeśli później zdecydujemy się zmienić sposób działania metod powiadamiających, możemy po prostu zmienić makro, a struktura każdej używającej go klasy ulegnie zmianie.