Lisp – Macros, definirne di nostre – 1

l0Adesso, dice Peter, è ora di definire le nostre macro. Panico 🙄 no, ormai dai :grin:. La difficoltà principale di capire le macro è forse che sono troppo integrate nel linguaggio. Ma Peter non si stanca di ripetere che “macros operate at a different level than functions and create a totally different kind of abstraction“. E ancora:

Once you understand the difference between macros and functions, the tight integration of macros in the language will be a huge benefit. But in the meantime, it’s a frequent source of confusion for new Lispers. The following story, while not true in a historical or technical sense, tries to alleviate the confusion by giving you a way to think about how macros work.

La storia di Mac, solo una storia

Premessa mia: Mac non è quello là; è una delle prime versioni al MIT, forse vuol dire Men And Computers, forse qualcos’altro. Allora non c’erano le macro e si doveva riscrivere e a quei tempi anche il copiancolla e –OK, la storiella è divertente, Peter la racconta benissimo, leggetela da lui. Ha anche un bel finale 😆

Macro Expansion Time vs. Runtime

Il segreto di capire le macro è di aver chiara la distinzione quando il codice che genera codice (quello della macro) e il codice che finisce per diventare il programma (tutto il resto). Quando si scrive una macro si scrive codice che sarà usato dal compilatore per generare codice che sarà compilato. Solo dopo che tutte le macros sono espanse e il codice risultante è compilato il programma può girare. Il momento in cui le macros girano è chiamato macro expansion time; è distinto dal runtime quello in cui il codice –incluso quello generato dalle macro– gira. La distinzione è importante –e facile, la do per scontata 🙄 Supponiamo che nel programma ci sia:

(defun foo (x)
  (when (> x 10) (print 'big)))

Normalmente viene da pensare a x come una variabile con un valore che viene passato a foo. Ma quando la macro viene espansa, vale a dire quando il compilatore fa girare la macro when, è disponibile solo il codice sorgente. Siccome il programma non sta girando non c’è nessun valore associato a x. Supponiamo che when sia (come abbiamo già visto) così definita:

(defmacro when (condition &rest body)
  `(if ,condition (progn ,@body)))

Quando il codice viene compilato la macro when viene eseguita con queste due forms come argomenti. Il parametro condition viene collegato a (> x 10) e la form (print 'big) viene raccolta in una lista che diventa il valore del corpo del parametro &rest. La backquote genererà questo codice: (if (> x 10) (progn (print 'big))) interpolando tra il valore di condition e unendo i valori del corpo con progn. Quando il Lisp è interpretato, invece di compilato, la distinzione tra i tempi di espansione delle macros e quello di runtime è meno chiara perché collegate. Inoltre il linguaggio non specifica come devono essere trattate le macro ma in ogni caso who cares come direbbero i west-padagni indigeni locali di qui 😉

defmacro

Le macros vengono definite con defmacro che sta per DEFine MACRO; lo schema base è simile a quello di defun:

(defmacro name (parameter*)
  "Optional documentation string."
  body-form*)

Siccome le macros possono usare tutto quanto il Lisp qui ce ne sarà solo un pochino ma verrà descritto il processo per scrivere macros, dalla più semplice alla più complessa. Il compito di una macro è di tradurre una macro form in codice che fa una particolare cosa. Normalmente si parte scrivendo il codice che si vuole scrivere, cioè con un esempio della macro form. Altre volte si decide di scrivere la macro dopo aver scritto più volte lo stesso codice e si capisce che è il momento di astrarre. In ogni caso devi sapere sia da dove vieni e dove vuoi arrivare prima di sperare di scrivere il codice che lo fa automaticamente. Allora il primo passo è di scrivere almeno un esempio di una chiamata alla macro e di come sarà espansa. Quando si ha l’esempio di chiamata e di come viene espansa si passa al secondo step: scrivere il codice della macro. Per una macro semplice questo si riduce a scrivere un backquoted template con i parametri inseriti nel loro posto. Le macros complesse sono molto più impegnative. 👿 Dopo aver scritto il codice per tradurre la chiamata d’esempio nell’espansione appropriata ti devi assicurare che l’astrazione non perda (leak) dettagli nella sua implementazione. Con un’astrazione leaky la macro funziona con certi argomenti ma con altri interagisce con l’ambiente in modo indesiderato. Finisce che le macros possono perdere in diversi modi, tutti che possono essere facilmente evitati se si sa come controllarne la presenza. Cosa che verrà discussa in “Tappare le perdite” Nota: detesto queste metafore da idraulico, non sono Mario. Riassumendo, i passi per scrivere una macro sono:

  • scrivere una semplice chiamata e il codice che dev’essere espando, o vice versa;
  • scrivere il codice che genera l’espansione scritta a mano nell’esempio di chiamata;
  • assicurarsi che l’astrazione della macro non perda.

Una macro d’esempio: do-primes

Per vedere come i tre passi enunciati vengono messi in pratica scriviamo la macro do-primes che con un ciclo similare a dotimes e dolist eccetto che invece di iterare con interi o elementi di una lista itera per numeri primi successivi. Non è una macro particolarmente utile, solo un esempio per vedere come si fa. Per prima cosa servono due funzioni d’utilità, una che ci dice se un numero è primo e un’altra che ci ritorna il primo successivo maggiore o uguale ai suoi argomenti. In entrambi i casi si può ricorrere all’inefficiente forza bruta:

(defun primep (number)
  (when (> number 1)
    (loop for fac from 2 to (isqrt number) never (zerop (mod number fac)))))

(defun next-prime (number)
  (loop for n from number when (primep n) return n))

Nota mia: primep andrebbe esaminata attentamente 🙄 Adesso si può scrivere la macro. Seguendo la procedura definita prima ci serve un esempio di chiamata e di espansione. Ecco:

(do-primes (p 0 19)
  (format t "~d " p))

per esprimere un ciclo che esegue il corpo una volta per ognuno dei primi maggiori o uguali a 0 e minori o uguali a 19, con la variabile p che contiene il numero primo. Conviene seguire il modello delle macro dotimes e dolist; le macros che seguono un modello di macros esistenti sono più facili da capire di quelle che introducono una sintassi nuova. Senza la macro do-primes si potrebbe scrivere un ciclo con do e le due utility definite prima come questo:

(do ((p (next-prime 0) (next-prime (1+ p))))
    ((> p 19))
  (format t "~d " p))

Siamo adesso pronti per scrivere il codice che traduce dal precedente a quest’ultimo.

Parametri delle macros

Siccome gli argomenti passati a una macro sono oggetti Lisp rappresentanti il codice sorgente della chiamata il primo passo per ogni macro è di estrarre quali sono le parti che servono per calcolare l’espansione. Per macros che interpolano semplicemente i loro argomenti in un template questo passo è banale: definire i parametri giusti per contenere i differenti argomenti è sufficiente. Ma questo approccio non sembra essere sufficiente per do-primes. Il primo argomento nella chiamata a do-primes è una lista contenente il nome della variabile del ciclo, p; il limite inferiore, 0; e il limite superiore, 19. Ma guardando all’espansione la lista come tale non appare nell’espansione; i tre elementi sono divisi e messi in posti diversi. Si può definire do-primes con due parametri, uno per contenere il primo e un parametro &rest per le forms del corpo, e quindi dividere la lista, in qualcosa di simile:

(defmacro do-primes (var-and-range &rest body)
  (let ((var (first var-and-range))
        (start (second var-and-range))
        (end (third var-and-range)))
    `(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
         ((> ,var ,end))
       ,@body)))

Tra un attimo la spiegazione di come il corpo generi l’espansione corretta; per adesso si noti come var, start e end hanno ognuna il valore, estratto da var-and-range, che viene interpolato nella backquote che genera l’espansione di do-primes. Tuttavia non è necessario dividere var-and-range a mano perché le liste dei parametri delle macro sono quelle che vengono chiamate destructuring parameter lists. Come il nome suggerisce involvono il suddividere la struttura, in questo caso la lista delle forms passate alla macro. Con una destructuring parameter list un semplice parametro può essere rimpiazzato in una lista di parametri annidata. I parametri nella lista di parametri annidata prendono i loro valori dagli elementi dell’espressione che sarebbero stati collegati nella lista rimpiazzata. Per dire si può rimpiazzare var-and-range con la lista (var start end) e i tre elementi verranno automaticamente destrutturati in questi tre parametri. Un’altra caratteristica speciale delle macro parameter lists è che si può usare &body come sinonimo di &rest. Semanticamente &body e &rest sono equivalenti ma diversi ambienti usano &body per modificare l’indentazione usata per le macro. Così si può snellire la definizione di do-primes e dare un suggerimento sia al programmatore che legge sia al tool di sviluppo definendo la macro così:

(defmacro do-primes ((var start end) &body body)
  `(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
       ((> ,var ,end))
     ,@body))

Oltre che essere più concisa la destructuring parameter lists esegue un controllo automatico degli errori. Con do-primes definito in questo modo Lisp sarà capace di trovare le chiamate in cui il primo argomento non è una lista di tre elementi e dare un messaggio d’errore chiaro di chiamata a funzione con troppi pochi argomenti. Inoltre se si unsa SLIME (Emacs) che i indica quali argomenti ci si aspettano appena scrivi il nome della funzione o macro con la destructuring parameter lists sarà in grado di essere più esplicito della sintassi della chiamata alla macro. Con la definizione originale SLIME direbbe che do-primes è chiamata come: (do-primes var-and-range &rest body) ma con la nuova definizione può irti che dev’essere così: (do-primes (var start end) &body body) Le destructuring parameter lists possono contenere parametri &optional, &key e &rest e destructuring parameter lists annidate. Cose non richieste nel nostro esempio.

Generare l’espansione

Siccome do-primes è semplice e abbiamo destrutturato gli argomenti resta solo da interpolarli nel template per avere l’espansione. Per le macro semplici la sintassi di backquote è perfetta. Ci ricordiamo vero che un’espressione backquotata è come una quotata tranne che si può squotare una subespressione precedendola con una virgola e per le liste un ,@. Un altro modo di vedere la backquote è come metodo conciso di scrivere codice che genera liste. Questo è esattamente quel che succede quando il reader legge l’espressione backquotata, la traduce nel codice che genera la struttura appropriata. Per esempio `(,a b) può essere letta come (list a 'b). Lo standard del linguaggio non specifica cosa il reader deve produrre. Nota: capito poco. Ecco una tabella con alcuni esempi

Backquote Syntax    Equivalent List-Building Code  Result
`(a (+ 1 2) c)	    (list 'a '(+ 1 2) 'c)	   (a (+ 1 2) c)
`(a ,(+ 1 2) c)	    (list 'a (+ 1 2) 'c)	   (a 3 c)
`(a (list 1 2) c)   (list 'a '(list 1 2) 'c)	   (a (list 1 2) c)
`(a ,(list 1 2) c)  (list 'a (list 1 2) 'c)	   (a (1 2) c)
`(a ,@(list 1 2) c) (append (list 'a) (list 1 2)   (list 'c)) (a 1 2 c)

È importante notare che la backquote è solo una convenienza ma ne è una grossa. Per apprezzarla compariamo la versione backquotata di do-primes data sopra con quella che usa le liste esplicite:

(defmacro do-primes-a ((var start end) &body body)
  (append '(do)
          (list  (list (list var
                             (list 'next-prime start)
                             (list 'next-prime (list '1+ var)))))
          (list (list (list '> var end)))
          body))

Vedremo prossimamente che l’implementazione corrente di do-primes non gestisce correttamente un certo caso limite. Ma per iniziare vediamo che funzioni per l’esempio originale. si può fare il test in due modi. si può provare se va, ecco: l8-0 OK. Oppure si può controllare la macro direttamente nella sua espansione di una particolare chiamata. La funzione macroexpand-1 prende una qualsiasi espressione Lisp come argomento e ritorna il risultato di un livello di espansione di macro. C’è anche la funzione macroexpand che continua a espandere ma visualizza molti più dettagli a basso livello, panicosa 😉 Poiché macroexpand-1 è una funzione per passargli un argomento letterale bisogna quotarlo. Provo: l8-1 OK. Se si usasse SLIME (mi sa che prima o poi…) poni il cursore nella parentesi aprente la fporm della macro e digiti C-c RET e il risultato è stampato in un buffer. In ogni caso sembra che tutto funzioni. Ma … –prossimamente :mrgreen:

Posta un commento o usa questo indirizzo per il trackback.

Trackback

Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo di WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione /  Modifica )

Google photo

Stai commentando usando il tuo account Google. Chiudi sessione /  Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione /  Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione /  Modifica )

Connessione a %s...

Questo sito utilizza Akismet per ridurre lo spam. Scopri come vengono elaborati i dati derivati dai commenti.

%d blogger hanno fatto clic su Mi Piace per questo: