Common Lisp Macro/1

Tra le funzionalità che rendono il Common Lisp un linguaggio molto
potente e differente
da quelli più diffusi, vi sono
le macro.Una notazione ed una sintassi con poche regole ed un sistema di
lettura, compilazione e esecuzione del codice molto flessibile danno
allo sviluppatore la possibilità di astrarre un pattern di codice in
un nuovo costrutto, laddove tale pattern non può essere astratto con
una tradizionale funzione. I lettori che già conoscono il Common Lisp
possono saltare il prossimo paragrafo, nel quale ne sono riassunte
brevemente le basi. Coloro i quali vogliono approfondire o conoscere le macro in Scheme possono visionare le Avventure di un Pythonista in Schemeland 8, 9,10 e 11.

Background

La sintassi Lisp si poggia sulla definizione del suo tipo di dato
fondamentale: la lista. Senza entrare nel dettaglio, una
lista in Lisp é
una lista
concatenata
, delimitata da parentesi tonde dove i blocchi,
concatenati mediante puntatori, si chiamano cons (si veda anche la più  dettagliata introduzione negli ottimi articoli di Michele Simionato) . Tale
tipo di dato permette di descrivere insiemi dove gli elementi sono
oggetti semplici o liste a loro volta.

Tutto ciò non è molto diverso dalle analoghe strutture presenti in
molti altri linguaggi di programmazione, se non per il fatto che un
programmatore Lisp utilizza le liste, oltre che per memorizzare dati,
anche per descrivere le operazioni sui dati.

Il codice infatti necessario per eseguire un’operazione (ad es. una
funzione) su un insieme di dati (i parametri) é costituito da una
lista (o form) in cui il primo elemento é un simbolo,
identificante l’operatore, e i rimanenti elementi sono i suoi
argomenti, i quali (come nella maggior parte dei i linguaggi) vengono
sempre valutati prima dell’esecuzione dell’operatore stesso.

Per rappresentare, ad esempio, l’operazione aritmetica comunemente
scritta come 2 * (1 + foo) si scrive (* 2 (+ 1
foo))
, dove il simbolo foo verrà valutato ovvero
inteso come variabile associata ad un qualche valore, al momento della
esecuzione della somma.

Per costruire e memorizzare una lista di dati, invece, si utilizza
l’operatore list.
Ad esempio (list 2 "Hello, World!" (list 5 'foo) 11)
costruisce una lista contenente numeri, stringhe ed una sottolista
con 5 e il simbolo foo, dove il
'(quote) indica che foo non deve essere
valutato ma inteso solo come simbolo.

Si noti a tal punto la differenza tra le form (list '+ 1 2
3)
, e la form (+ 1 2 3). La prima costruisce una
lista con il simbolo +,1,2
e 3, mentre la seconda esegue questa operazione.

Tale osservazione suggerisce che è possibile costruire delle liste che
descrivono delle operazioni, che potranno essere eseguite, o
manipolate ulteriormente. Tale caratteristica è
chiamata omoiconicità.

In seguito si utilizzeranno i seguenti operatori:

(let
((foo val)) body...)
esegue il codice contenuto in body
(dove body è un insieme di form body ===
form1 form2 form3 ...
) in un ambiente lessicale in cui la
variabile foo ha il valore val.
(funcall
foo)
Esegue la funzione definita da foo.
(prog2
body...)

e (progn
body...)
eseguono sequenzialmente le form contenute in body
restituendo rispettivamente il valore della seconda e
dell’ultima form. Es. la valutazione della form (prog2
(foo) (bar) (baz))
consiste nella chiamata
a foo, bar e baz
nell’ordine prescritto e il valore restituito dall’intero blocco
è il valore restituito da bar.

(unwind-protect
form1 body...)
Esegue la form form1 e si assicura che siano
eseguite le form body anche se in form1
c’è un salto (derivante da un goto, un return, un’ eccezione, la
gestione esterna di una condizione dovuta ad un segnale).
(setq
name value)
Associa al simbolo name il
valore value, ovvero assegna alla
variabile name il valore value.

La seconda caratteristica che rende particolare il Common Lisp è il
modo in cui i sorgenti vengono letti e compilati.

Durante la compilazione (fase anche detta di macro
espansione) l’utente può infatti eseguire del codice in modo da
cambiare (generare, includere, rimuovere, modificare) le porzioni di
codice che verranno effettivamente compilate. Essenzialmente nulla di
diverso dai sistemi di preprocessing presenti in molti linguaggi (ad
es. C), ma a differenza di questi, dove si é costretti ad usare un
sottolinguaggio molto povero (ad
esempio #ifdef, #define), in Common Lisp è
possibile utilizzare tutto il linguaggio.

Questa possibilità unita alla caratteristica di descrivere dati e
codice mediante la stessa struttura dati fornisce all’utente un nuovo
strumento per costruire astrazioni e quindi codice più compatto e
leggibile.

defmacro

L’operatore Common
Lisp defmacro
nella form (defmacro foo (arg1 arg2...) form) definisce
una macro che stabilisce che ogni qualvolta che il compilatore
incontra la form (foo value1 value2), questa viene
sostituita dal risultato, calcolato durante la
compilazione, di form. Tale risultato che può
eventualmente dipendere da value1 e value2,
solitamente è una lista con il codice che verrà effettivamente
eseguito.

Un esempio unicamente a scopo esemplificativo è
costituito dalla macro:

  (defmacro mydouble (val) 
(list '+ val val))

che fa sì che il codice (mydouble bar) sia compilato
come (+ bar bar). Il risultato di una espansione di un macro
può essere a sua volta oggetto di una nuova regola e quindi di una
nuova espansione di una macro, così come è possibile avere regole di
macro ricorsive. Per visualizzare il codice effettivamente
compilato è possibile usare macroexpand-1
e macroexpand,
i quali data una form restituiscono rispettivamente l’espressione
effettivamente compilata dopo una sola macroespansione o tutte quelle
definite.

Aggiungere nuovi costrutti di controllo

Un ambito di utilizzo decisamente più comune di una macro è la
creazione di nuovi costrutti di controllo del flusso dell’esecuzione.

Scrivere una macro significa scrivere una regola di riscrittura del
codice, ovvero significa avere la possibilità di poter costruire un
operatore che modifica sintatticamente i propri
argomenti. Ciò fa si che sia possibile costruire degli operatori che
accettano come parametro un blocco di codice (un body) di
cui ne viene modificano il comportamento.

Volendo ad esempio costruire un costrutto di tipo (my-if test
true-code false-code)
che oltre ad eseguire la
form true-code (o false-code)
quando test è vero (o falso), traccia il valore
di test, scriveremo:

(defmacro my-if (test true-code false-code)
(list 'if (list 'log-somewhere test)
true-code
false-code))

dove log-somewhere è una funzione che traccia il valore
del suo argomento ad esempio in un file. Digitando quindi
l’espressione:

(my-if (= 1 1)
(print "Ok tutto funziona")
(print "Aiuto!!!!"))

Questa verrà compilata come se fosse stato digitato:

(if (log-somewhere (= 1 1))
(print "Ok tutto funzione")
(print "Aiuto!!!!"))

Si noti che non sarebbe possibile ottenere lo stesso comportamento con
la stessa sintassi di my-if con una funzione senza
evitare l’esecuzione di entrambi i print.

Il Common Lisp permette di definire my-if anche con una
sintassi più concisa, che d’ora in avanzi utilizzeremo:

(defmacro my-if (test true-code false-code)
`(if (list log-somewhere ,test)
,true-code
,false-code))

Tale definizione è perfettamente equivalente alla precedente dove
il ` (backquote) indica al compilatore di non valutare gli
elementi della lista se non quelli preceduti da , (virgola).

Le macro possono essere un efficace strumento per definire strutture
di controllo di flusso personalizzate, senza nessun limite nella
complessità del codice effettivamente eseguito. La creazione di
costrutti particolarmente complessi che manipolano i propri argomenti
definiscono di fatto dei veri e propri
linguaggi embedded, che possono risultare più adatti a
descrivere il dominio del nostro problema.

Un esempio famoso e particolarmente sofisticato è la
macro loop
che permette di scrivere codice come:

(loop for i = (funcall fib)
until (> i 1000000)
when (evenp i)
sum i)))

estratto dalla soluzione di Valentino Volonghi al problema
di Eulero 2. La macro loop costruisce le
giuste istruzioni Lisp (che in tal caso sarebbero costituite da una
lunga e annidata serie di operazioni su liste) utilizzando i suoi
argomenti laddove opportuno. La sua implementazione, inclusa in ogni
compilatore Common Lisp sin dagli anni 80, è un vero e
proprio programma.

To be continued…

Nella seconda parte vedremo alcuni tipici utilizzi delle macro Common Lisp

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.