Ricerca…


Osservazioni

Lo scopo delle macro

Le macro sono pensate per generare codice, trasformare codice e fornire nuove notazioni. Queste nuove annotazioni possono essere più adatte per esprimere meglio il programma, ad esempio fornendo costrutti a livello di dominio o interi nuovi linguaggi incorporati.

I macro possono rendere il codice sorgente più intuitivo, ma il debug può essere reso più difficile. Come regola generale, non si dovrebbero usare macro quando si eseguirà una funzione regolare. Quando li usi, evita le solite insidie, prova ad attenersi ai modelli e convenzioni di denominazione comunemente usati.

Ordine di espansione macro

Rispetto alle funzioni, le macro vengono espanse in ordine inverso; prima il più grande, l'ultimo. Ciò significa che per impostazione predefinita non è possibile utilizzare una macro interna per generare la sintassi richiesta per una macro esterna.

Ordine di valutazione

A volte le macro devono spostare i moduli forniti dagli utenti. Bisogna essere sicuri di non cambiare l'ordine in cui vengono valutati. L'utente potrebbe basarsi su effetti collaterali che accadono in ordine.

Valuta solo una volta

L'espansione di una macro spesso ha bisogno di utilizzare il valore dello stesso modulo fornito dall'utente più di una volta. È possibile che la forma abbia effetti collaterali, oppure potrebbe chiamare una funzione costosa. Pertanto, la macro deve assicurarsi di valutare solo tali moduli una sola volta. Di solito questo sarà fatto assegnando il valore a una variabile locale (il cui nome è GENSYM ed).

Funzioni utilizzate da Macro, utilizzando EVAL-WHEN

Le macro complesse spesso hanno parti della loro logica implementate in funzioni separate. È necessario ricordare, tuttavia, che le macro vengono espanse prima della compilazione del codice effettivo. Durante la compilazione di un file, per impostazione predefinita, le funzioni e le variabili definite nello stesso file non saranno disponibili durante l'esecuzione della macro. Tutte le definizioni di funzioni e variabili, nello stesso file, utilizzate da una macro devono essere racchiuse all'interno di una forma EVAL-WHEN . L' EVAL-WHEN dovrebbe avere tutte e tre le volte specificato, quando il codice allegato deve essere valutato anche durante il caricamento e il runtime.

(eval-when (:compile-toplevel :load-toplevel :execute)
  (defun foobar () ...))

Questo non si applica alle funzioni chiamate dall'espansione della macro, solo quelle chiamate dalla macro stessa.

Modelli macro comuni

TODO: Forse sposta le spiegazioni in commento e aggiungi esempi separatamente

FOOF

In Common Lisp, c'è un concetto di riferimenti generalizzati . Consentono a un programmatore di impostare valori su vari "luoghi" come se fossero variabili. Le macro che fanno uso di questa abilità spesso hanno un F -postfix nel nome. Il posto è solitamente il primo argomento della macro.

Esempi dallo standard: INCF , DECF , ROTATEF , SHIFTF , REMF .

Un esempio stupido, una macro che capovolge il segno di un negozio di numeri in un luogo:

(defmacro flipf (place)
  `(setf ,place (- ,place)))

CON-FOO

Le macro che acquisiscono e rilasciano in modo sicuro una risorsa vengono solitamente denominate con un WITH- WITH-. La macro dovrebbe in genere utilizzare la sintassi come:

(with-foo (variable details-of-the-foo...)
  body...)

Esempi dallo standard: WITH-OPEN-FILE , WITH-OPEN-STREAM , WITH-INPUT-FROM-STRING , WITH-OUTPUT-TO-STRING .

Un approccio per l'implementazione di questo tipo di macro che può evitare alcune delle insidie ​​dell'inquinamento del nome e della valutazione multipla non intenzionale consiste nell'implementare prima una versione funzionale. Ad esempio, il primo passaggio nell'implementazione di una macro with-widget che crea in modo sicuro un widget e che pulisce in seguito potrebbe essere una funzione:

(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

Poiché si tratta di una funzione, non vi sono dubbi sull'ambito dei nomi all'interno della funzione o del fornitore e semplifica la scrittura di una macro corrispondente:

(defmacro with-widget ((var &rest args) &body body)
  `(call-with-widget (list ,@args) (lambda (,var) ,@body)))

DO-FOO

Le macro che eseguono l'iterazione su qualcosa vengono spesso denominate con un DO -prefix. La sintassi della macro dovrebbe di solito essere in forma

(do-foo (variable the-foo-being-done return-value)
  body...)

Esempi dallo standard: DOTIMES , DOLIST , DO-SYMBOLS .

FOOCASE, EFOOCASE, CFOOCASE

Le macro che corrispondono a un input rispetto a determinati casi vengono spesso denominate con un CASE -postfix. C'è spesso un E...CASE -variant, che segnala un errore se l'input non corrisponde a nessuno dei casi, e C...CASE , che segnala un errore continuativo. Dovrebbero avere la sintassi come

(foocase input
  (case-to-match-against (optionally-some-params-for-the-case)
   case-body-forms...)
  more-cases...
  [(otherwise otherwise-body)])

Esempi dello standard: CASE , TYPECASE , HANDLER-CASE .

Ad esempio, una macro che corrisponde a una stringa rispetto alle espressioni regolari e associa i gruppi di registri alle variabili. Utilizza CL-PPCRE per le espressioni regolari.

(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

Le macro che definiscono le cose di solito sono denominate con DEFINE- o DEF -prefix.

Esempi dallo standard: DEFUN , DEFMACRO , DEFINE-CONDITION .

Macro anaphoric

Una macro anaforica è una macro che introduce una variabile (spesso IT ) che cattura il risultato di un modulo fornito dall'utente. Un esempio comune è Anaphoric If, che è come un IF regolare, ma definisce anche la variabile IT per fare riferimento al risultato del test-form.

(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)!

MACROEXPAND

L'espansione macro è il processo di trasformazione delle macro in codice reale. Questo di solito accade come parte del processo di compilazione. Il compilatore espande tutte le macro prima di compilare effettivamente il codice. L'espansione delle macro avviene anche durante l' interpretazione del codice Lisp.

Si può chiamare MACROEXPAND manualmente per vedere a cosa si espande un modulo macro.

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 è lo stesso, ma si espande solo una volta. Questo è utile quando si cerca di dare un senso a una macro che si espande in un'altra forma macro.

CL-USER> (macroexpand-1 '(with-open-file (file "foo")
                          (do-something-with file)))
(WITH-OPEN-STREAM (FILE (OPEN "foo")) (DO-SOMETHING-WITH FILE))

Nota che né MACROEXPANDMACROEXPAND-1 espandono il codice Lisp su tutti i livelli. Espande solo il modulo macro di livello superiore. Per espandere a macroistruzione un modulo completamente su tutti i livelli, è necessario un walker di codice per farlo. Questa funzione non è fornita nello standard Common Lisp.

Backquote - scrittura di modelli di codice per macro

Le macro restituiscono il codice. Poiché il codice in Lisp è costituito da liste, è possibile utilizzare le normali funzioni di manipolazione delle liste per generarlo.

;; A pointless macro
(defmacro echo (form)
  (list 'progn
        (list 'format t "Form: ~a~%" (list 'quote form))
        form))

Questo è spesso molto difficile da leggere, specialmente nei macro più lunghi. La macro del lettore Backquote consente di scrivere modelli quotati che vengono compilati valutando gli elementi in modo selettivo.

(defmacro echo (form)
  `(progn
     (format t "Form: ~a~%" ',form)
     ,form))

(macroexpand '(echo (+ 3 4)))
;=> (PROGN (FORMAT T "Form: ~a~%" '(+ 3 4)) (+ 3 4))

Questa versione sembra quasi un normale codice. Le virgole sono utilizzate per valutare la FORM ; tutto il resto viene restituito così com'è. Si noti che in ',form la virgoletta singola al di fuori della virgola, quindi verrà restituita.

Si può anche usare ,@ per unire una lista nella posizione.

(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))

Anche il backquote può essere usato al di fuori dei macro.

Simboli unici per evitare conflitti di nomi nelle macro

L'espansione di una macro spesso richiede l'uso di simboli che non sono stati passati come argomenti dall'utente (come i nomi delle variabili locali, ad esempio). Bisogna assicurarsi che tali simboli non possano entrare in conflitto con un simbolo che l'utente sta usando nel codice circostante.

Solitamente ciò si ottiene usando GENSYM , una funzione che restituisce un nuovo simbolo non integrato.

Male

Considera la macro qui sotto. Fa un DOTIMES -loop che raccoglie anche il risultato del corpo in una lista, che viene restituita alla fine.

(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)

Questo sembra funzionare in questo caso, ma se l'utente avesse un nome variabile RESULT , che utilizza nel corpo, i risultati probabilmente non sarebbero ciò che l'utente si aspetta. Considera questo tentativo di scrivere una funzione che raccoglie un elenco di somme di tutti gli interi fino a N :

(defun sums-upto (n)
  (let ((result 0))
    (dotimes+collect (i n)
      (incf result i))))

(sums-upto 10) ;=> Error!

Buono

Per risolvere il problema, abbiamo bisogno di usare GENSYM per generare un nome univoco per il RESULT -variable nella macro di espansione.

(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)

TODO: Come creare simboli dalle stringhe

TODO: evitare problemi con i simboli in diversi pacchetti

se-lascia, quando-lascia, -let macro

Queste macro uniscono flusso di controllo e associazione. Sono un miglioramento rispetto alle macro anaforiche anaforiche perché consentono allo sviluppatore di comunicare il significato attraverso la denominazione. Come tale il loro uso è raccomandato sulle loro controparti anaforiche.

(if-let (user (get-user user-id))
  (show-dashboard user)
  (redirect 'login-page))

FOO-LET macro di FOO-LET associano una o più variabili e quindi usano quelle variabili come modulo di test per il condizionale corrispondente ( IF , WHEN ). Più variabili sono combinate con AND . Il ramo scelto viene eseguito con i binding in vigore. Un'implementazione di una sola variabile di IF-LET potrebbe assomigliare a qualcosa:

(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.~%")))

Una versione che supporta più variabili è disponibile nella libreria di Alessandria .

Utilizzo delle macro per definire le strutture dati

Un uso comune delle macro è creare modelli per strutture dati che rispettano regole comuni ma che possono contenere campi diversi. Scrivendo una macro, è possibile consentire la configurazione dettagliata della struttura dati da specificare senza la necessità di ripetere il codice boilerplate, né utilizzare una struttura meno efficiente (come un hash) in memoria per semplificare semplicemente la programmazione.

Ad esempio, supponiamo di voler definire un numero di classi che hanno una gamma di proprietà diverse, ciascuna con un getter e un setter. Inoltre, per alcune (ma non tutte) di queste proprietà, desideriamo che il setter chiami un metodo sull'oggetto che lo informa che la proprietà è stata cambiata. Sebbene Common LISP abbia già una scorciatoia per scrivere getter e setter, la scrittura di un setter personalizzato standard in questo modo richiederebbe normalmente la duplicazione del codice che chiama il metodo di notifica in ogni setter, il che potrebbe essere un problema se ci sono un gran numero di proprietà coinvolte . Tuttavia, definendo una macro diventa molto più semplice:

(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)))

Ora possiamo scrivere (defclass-notifier-slots foo (bar baz qux) (waldo)) e definire immediatamente una classe foo con un normale slot waldo (creato dalla seconda parte della macro con le specifiche (waldo :accessor waldo) ) e la bar slot, baz e qux con i setter che chiamano il metodo changed (in cui il getter è definito dalla prima parte della macro, (bar :reader bar) e il setter dalla macro del notifier richiamato).

Oltre a permetterci di definire rapidamente più classi che si comportano in questo modo, con un gran numero di proprietà, senza ripetizione, abbiamo il solito vantaggio del riutilizzo del codice: se in seguito decidiamo di cambiare il modo in cui funzionano i metodi del notificatore, possiamo semplicemente cambiare il macro, e la struttura di ogni classe che lo usa cambierà.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow