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.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow