Common Lisp Macro/2

In questa seconda parte vedremo alcuni utilizzi delle macro in Common Lisp
e discuteremo due problemi tipici: la cattura accidentale di nomi (variable capture) e la valutazione
multipla.


Decorare il codice

Un utilizzo comune delle macro è la costruzione
di wrapper che ci permettono di modificare il
comportamento di un blocco di codice, magari di terze parti, senza
modificare direttamente la definizione di questo. Supponiamo ad
esempio di voler definire un costrutto del linguaggio che esegue un
blocco di codice fornito come parametro e effettua alcune
operazioni all’inizio (di setup) e al termine (di teardown)
dell’esecuzione del blocco. Ad esempio la macro:

(defmacro with-transaction (&body body)
"Execute body in a transaction"
`(let ((done nil))
(unwind-protect
(prog2
(begin-transaction)
(progn ,@body)
(setq done t))
(if done
(commit-transaction)
(abort-transaction)))))

rende possibile scrivere in maniera concisa:

(with-transaction
(establish-connection)
(do-some-calculation)
(transfer-a-lot-of-money))

dove ,@ (comma-at, detto anche snail o splice-unquote) fa
si che ,@body sia sostituito dalle form contenute
in body.

Nel codice precedente, le
form (establish-connection),
(do-some-calculation)
e (transfer-a-lot-of-money)
saranno valutate dopo la form begin-transaction e dopo di
queste, a seconda del risultato dell’operazione, sarà valutata
commit-transaction o abort-transaction.

Tale esecuzione é garantita ovvero avviene anche qualora all’interno delle 3 funzioni nel body vengano generati errori, oppure vengono eseguiti dei goto non locali. Usi tipici di questo pattern sono nella gestione delle sessioni, degli
accessi a file o a database, delle connessioni di rete e sono una dimostrazione di cosa si può ottenere combinando le macro con costrutti di controllo del flusso come unwind-protect .

La combinazione, invece, dell’uso delle macro con le variabili globali a binding dinamico (per
maggiori dettagli si veda qui) dà luogo ad altre macro della forma with-*, che tipicamente sono utilizzate quando si vuole eseguire un
blocco di codice in un nuovo ambiente lessicale, in cui (e solo al suo
interno) una variabile sia associata ad un particolare valore. Esempi
reali sono il redirezionamento di uno stream (l’esempio é tratto dalla
libreria arnesi) e
l’applicazione di trasformazioni geometriche nel disegno vettoriale
(tratto
da McCLIM):

(with-output-to-file "/home/foo/mylog.log"
(do-something)
(print "Hello World")
(do-something-again))
(with-rotation (graphical-stream -90 (make-point 10 5))
(with-translation (graphical-stream 20 30)
(draw-arrow graphical-stream (make-point 0 5) (make-point 0 10))))

Il primo snippet fa sì che tutte le operazioni contenute nel blocco
siano eseguite e l’output prodotto (ad esempio quello
di una print) sia redirezionato al
file "/home/foo/mylog.log". print infatti
invia l’output allo stream associato alla
variabile *standard-output*. Questa è solitamente assegnata ad uno stream associato al nostro terminale ma nel blocco sopracitato è associata ad un file. Per inciso la definizione
di with-output-to-file sarà qualcosa del tipo:

(defmacro with-output-to-file (file &body body)
`(let ((*standard-output* (open file :direction :output)))
,@body
(close *standard-output*)))

Il secondo esempio disegna una freccia tramite draw-arrow
dove le coordinate sono fornite in modo tale da venir trasformate
dalla rotazione e dalla traslazione definite
da with-translation e with-rotation.

L’utilizzo di questa tecnica permette di scrivere codice molto
flessibile e configurabile. Chi costruisce librerie è solitamente a
conoscenza di tale possibilità e costruisce funzioni che, piuttosto
che accettare centinaia di parametri, siano configurabili mediante
variabili globali a binding dinamico. In sorgenti complessi tale tipo
di design ha un impatto positivo in termini di leggibilità e
manutenibilità .

Problemi

Scrivere le macro non é sempre un’operazione semplice, a causa di due
problemi causa di bug spesso difficili da individuare:
la variable capture e la valutazione
multipla. Si supponga di scrivere il codice seguente:

(let ((done 42))
(with-transaction
(incf done))
done)

il quale verrà espanso, prima di essere compilato in:

(let ((done 42))
(let ((done nil))
(unwind-protect
(prog2
(begin-transaction)
(progn (incf done))
(setq done t))
(if done
(commit-transaction)
(abort-transaction)))))

È chiaro che tale codice non funziona in quanto si è generato una
conflitto tra la variabile intera done incrementata
mediante incf nella transazione e la variabile dallo
stesso nome done usata dalla macro per uso interno come
flag booleano.

A tale problematica si unisce il problema della valutazione multipla,
che si verifica quando un parametro della macro viene sostituito più
volte, e quindi poi valutato più volte. I lettori di Stacktrace hanno già visto in azione questo problema nelle Avventure di un Pythonista in Schemeland.

Tale comportamento può essere
non desiderato se il parametro è una form con side-effects.

Il Common Lisp permette di risolvere il problema della variable capture tramite la
funzione gensym che permette di definire variabili con
nomi univoci. I valori restituiti dalla funzione gensym
(simboli unici del tipo #:G2432) sono utilizzati
come nomi delle nuove variabili introdotte dalla macro.

Per evitare la variable capture la macro with-transaction
deve essere riscritta nel modo seguente:

(defmacro with-transaction (&body body)
"Execute body in a transaction"
(let ((done (gensym)))
`(let ((,done nil))
(unwind-protect
(prog2
(begin-transaction)
(progn ,@body)
(setq ,done t))
(if ,done
(commit-transaction)
(abort-transaction))))))

e il codice trasformato

(let ((done 42))
(let ((#:G2432 nil))
(unwind-protect
(prog2
(begin-transaction)
(progn (incf done))
(setq #:G2432 t))
(if #:G2432
(commit-transaction)
(abort-transaction)))))

Non sempre il fenomeno della variable capture è indesiderato. Talvolta
è possibile sfruttarlo costruendo particolari macro dette anaforiche
che permettono di scrivere codice del tipo:

(when-is-beautiful (read-a-phrase-from-file "/usr/share/data/divina-commedia.txt")
(show it)
(print it))

in cui la macro when-is-beautiful esegue il
codice (show it) (print it) in un ambiente lessicale
(definito da let) in cui il simbolo it è
associato al valore definito da (read-a-phrase-from-file
"/usr/share/data/divina-commedia.txt")
. Ovviamente la presenza
di un meccanismo anaforico è solitamente documentato da chi fornisce
la macro, e solitamente tali macro hanno nel nome un prefisso
idiomatico (ad esempio aif, awhen, etc.).

 Nella prossima ed ultima puntata della miniserie vedremo altri tipici utilizzi delle macro e cercheremo di capire quando é pericoloso utilizzare tali construtti e quanto l’abuso delle macro possa aumentare la difficoltà di debugging.

Comments

  1. da non lisper: bell’articolo, peccato che come al solito rimango più affascinato dalle variabili con scope dinamico che dall’uso delle macro :)

    Il che però mi fa porre una domanda, guardando lo pseudocodice per witgh-output-to-file: ma nell’usare le macro quanta attenzione va posta per gestire il flusso di controllo mischiato tra macro e codice esterno e i resource leak?

    Nel caso specifico, potrebbe esserci un file che rimane aperto se viene sollevata un’eccezione, no?
    Al 99% immagino sia un non problema perché il codice reale ne avrà cura e c’è comunque il GC, ma mi chiedo: c’è effettivamente un problema possibile a cui fare attenzione?

    E una cosa che mi son sempre chiesto: ma gensym come evita leakage dovuti alla necessità di non generare mai lo stesso simbolo in modo threadsafe?
    Si tratta di una cosa standard e semplice o implementation dependant?

  2. Ovviamente se c’e’ un’eccezione va gestita. Ma e’ una cosa che avviene a runtime. Le macro agiscono prima. gensym solitamente lo usi a tempo di macroespansione, quindi non hai problemi a runtime in quanto quei simboli generati vivono solo durante la compilazione.

    Mi spiego meglio:
    nel caso specifico with-output-to-file avra’ un unwind-protect che gestira’ l’eccezione. I simboli generati da gensym saranno gia’ stati generati ed il relativo codice sara’ stato compilato. Una macro quindi non ti puo’ generare leakage a runtime ma al massimo quando compili il tuo codice.

    Nessuno ti vieta cmq di generare simboli a runtime con gensym (per qualche applicazione di calcolo simbolico).
    I thread non fanno parte dello standard, quindi la cosa sarebbe implementation-dependent.

    Spero di aver capito la domanda

Policy per i commenti: Apprezzo moltissimo i vostri commenti, critiche incluse. Per evitare spam e troll, e far rimanere il discorso civile, i commenti sono moderati e prontamente approvati poco dopo il loro invio.