Le avventure di un Pythonista in Schemeland /9

Non c’è limite al livello di sofisticazione che si può raggiungere con
le macro; in particolare è possibile definire delle macro di ordine
superiore, ovverossia delle macro che definiscono macro. Questa
tecnica permette uno stile di programmazione molto elegante, che però
può facilmente condurre a codice incomprensibile e indebuggabile.
Per evitare ciò,
sarò costretto ad introdurre dei tool di supporto. Sfortunamente tali tool non saranno standard, in
quanto la tradizione di Scheme è quella
di fornire degli strumenti di base estremamente potenti con cui è
possibile definire delle utilità che rendono la programmazione in
Scheme relativamente semplice e debuggabile, ma di non inserire
tali utilità direttamente nello standard. Questo significa che ogni
programmatore è obbligato a invertarsi dei tool di sviluppo personali
diversi da quelli di tutti gli altri.

Il sistema di macro non fa eccezione a questa filosofia e per esempio
non esistono nello standard strumenti di debugging per le macro tipo
il macroexpand del Common Lisp anche se sono facilissimi da
costruire. La cosa fastidiosa è che non sarebbe stato difficile
rendere gli strumenti di base più usabili. Ci possono essere varie
spiegazioni per questa omissione. Volendo essere cattivi, si potrebbe
pensare che sia stato fatto per obbigare gli studenti a svolgere i
compiti, o addirittura che sia stato fatto per rendere Scheme un
linguaggio per pochi eletti.

bikeshed.jpg

Più realisticamente io penso che la
ragione sia il famigerato bikeshed problem che affligge qualunque
progetto disegnato da più di una persona (ricordiamo che Scheme è un
linguaggio disegnato da un comitato, non c’è un Benevolent Dictator
For Life come in Python/Perl/Ruby): quando si tratta di proporre delle
funzionalità tecnicamente avanzate e che pochi possono capire, è
facilissimo ottenerne l’approvazione da parte della comunità. D’altra
parte, quando si tratta di funzionalità semplici e di uso comune,
ognuno ha un’idea diversa ed è praticamente impossibile ottenere
l’approvazione di alcunché. Come conseguenza lo standard più che
proporre strumenti usabili, propone strumenti generali su cui ognuno
possa costruire le astrazioni usabili che preferisce. In quanto
Pythonista questa filosofia non mi convince (nel senso che secondo me
un linguaggio dovrebbe fornire non soltanto le soluzioni più generali
possibili, ma anche delle soluzioni preconfezionate per i casi d’uso
più comuni, possibilmente in librerie standard) ma così è, e sono
costretto ad adeguarmi.

Cominciamo con le sfide

Prima di cominciare con le macro del secondo ordine,
chiudiamo i conti in sospeso. Nella scorsa
puntata avevo lanciato la sfida seguente: scrivere una macro che conta
il numero dei suoi argomenti. Vi avevo anche preannunciato che la
soluzione sfruttava un frutto diabolico. In questa puntata ve lo
mostro. Il trucco diabolico consiste nel definirsi una macro
ausiliaria ricorsiva che contiene un parametro aggiuntivo, un
accumulatore, che chiameremo counter:

(define-syntax count-args-helper
 (syntax-rules ()
  ((count-args-helper counter)
    counter)
  ((count-args-helper counter arg argN ...)
    (count-args-helper (+ 1 counter) argN ...))))

Notate come astutamente la seconda riga rimuove il primo parametro
dal pattern, incrementa il contatore di una unità e richiama la
macro sugli argomenti rimanenti, fino a che non ne rimane nessuno e
in quel caso ritorna il valore finale del contatore.

A questo punta diventa ovvio come definire un count-args generico:

(define-syntax count-args
   (syntax-rules ()
     ((count-args arg ...) (count-args-helper 0 arg ...))))

Potete agevolmente verificare che il tutto funziona:

> (count-args)
0
> (count-args 'arg1)
1
> (count-args 'arg1 'arg2 'arg3 'arg4)
4

Vi conviene ricordare il trucco diabolico di definirsi una macro ausiliaria
con un parametro aggiuntivo, perché è un trucco comunissimo. Lo potete
usare per risolvere la seguente challenge: scrivere una versione
potenziata di if (if+) che permette di scrivere cose tipo

(if+ cond-1? return-1
     cond-2? return-2
         ...
     else return-default)

In pratica if+ è una condizionale con meno parentesi che può essere implementata
in termini della condizionale primitiva cond:

(cond
  (cond-1? return-1)
  (cond-2? return-2)
       ...
  (else return-default))

Quando avete risolto questa challenge, generalizzate la soluzione per
funzionare con altri costrutti Scheme che coinvolgono coppie, in modo
da poter scrivere cond, case oppure syntax-rules con meno
parentesi. Risolta anche questa sfida rispondete alla seguente domanda:
perché tutto sommato è meglio tenersi le parentesi? 😉

Macro che definiscono macro

Come ho detto nell’introduzione, esistono macro che possono definire
altre macro, l’esempio canonico essendo il define-syntax dello
standard di Scheme. Purtroppo il define-syntax standand non è
abbastanza usabile per i miei scopi perché non ha funzionalità
di debugging e/o introspezione e sarò quindi costretto a definirmi
un define-syntax+ personale che è come secondo me il comitato avrebbe
dovuto definire il define-syntax. Per arrivare a tale scopo finale,
conviene partire con un esercizio di riscaldamento.

Considerate la macro seguente, in cui ho indicato il primo argomento
dei pattern con un underscore, come si vede spesso nel codice Scheme:

> (define-syntax define-syntax-simplified
    (syntax-rules ()
      ((_ name (arg ...) templ)
       (define-syntax name
         (syntax-rules ()
           ((_ arg ...) templ))))))

Si tratta di una macro che espande a codice che definisce una macro:
per vedere quello che sta succedendo, conviene quotare
l’espansione in questo modo:

> (define-syntax define-syntax-simplified
    (syntax-rules ()
      ((_ name (arg ...) templ)
       '(define-syntax name
         (syntax-rules ()
           ((_ arg ...) templ))))))

(attenti al piccolissimo apice prima del define-syntax interno!).
Così facendo si vede che per esempio

> (define-syntax-simplified couple (x) '(x x))

espande a

(define-syntax couple
   (syntax-rules ()
     ((_ x) '(x x))))

Togliendo la quotazione, la macro viene definita effettivamente. Il
problema di define-syntax-simplified è che la macro è stata
semplicata troppo (quotando Einstein, everything should be made as
simple as possible, but not simpler
) e funziona solo per macro con
un singolo pattern senza identificatori letterali. È soltanto un esempio
di riscaldamento.

Un syntax-rules potenziato

Una modo più solido per potenziare le macro di Scheme con delle
funzionalità di debugging è quello di potenziare syntax-rules
come segue:

(define-syntax syntax-rules+
 (syntax-rules ()
   ((_ (literal ...) ((name . args) templ) ...)
    (syntax-rules (<literals> <patterns> <expand> literal ...)
      ((_ <literals>) '(literal ...))
      ((_ <patterns>) '((... (... (name . args))) ...))
      ((name <expand> . args) 'templ) ...
      ((name . args) templ) ...
      ))))

Un esempio d’uso è il seguente:

> (define-syntax couple
    (syntax-rules+ ()
     ((couple x) '(x x))))

L’identificatore letterale <patterns> fornisce una
funzionalità di introspezione: se state lavorando con una macro
definita da terze parti (o anche da voi stessi, ma vi siete
dimenticati come funziona) vi permette di vedere i patterns accettati
in ingresso. Per esempio

> (couple <patterns>)
  ((couple x))

In maniera simile, <literals> permette di vedere gli identificatori
letterali che la macro è in grado di riconoscere (a parte <literals>,
<patterns> e <expand>):

> (couple <literals>)
  ()

L’identificatore letterale <expand> fornisce delle funzionalità
di debugging; quando passate <expand> come primo argomento ad una
delle macro definite da syntax-rules+, otterrete l’espansione
della macro:

> (couple <expand> 3)
  '(3 3)

L’ unica parte delicata di syntax-rules+ è la riga con i puntini
di sospensione (ellipsis):

((_ <patterns>) '((... (... (name . args))) ...))

Per aumentare la suspense e tenervi in sospeso,
rimando la spiegazione dei punti di sospensione
alla prossima puntata (sono malvagio! ;).
In questa puntata vi segnalo invece una sottigliezza:
il codice precedente funziona senza problemi in Ikarus, ma se state
usando qualche altra implementazione vi potrebbe servire qualche
accortezza per farlo girare: in particolare in DrScheme/MzScheme
(forse l’implementazione più popolare di Scheme) è necessario
salvare la definizione di syntax-rules+ in un modulo ed
importarla con require-for-syntax per renderla visibile
al sistema di macro.

Concludo raccomandando quella che secondo me è la
migliore referenza che si può trovare in giro su syntax-rules, il
Syntax-Rules Primer for the Merely Eccentric, di Joe Marshall. Il
titolo è un gioco di parole sul saggio di Al Petrofsky An Advanced
Syntax-Rules Primer for the Mildly Insane
.

mad-scientist.jpg

Il livello del saggio di
Marshall è molto alto, si rivolge ad esperti programmatori Scheme,
quindi può valere la pena di leggersi altre puntate prima di
affrontarlo. D’altra parte, è un gioco da ragazzi al confronto del
saggio di Petrofsky che si rivolge dichiaratamente ai guru a cinque
stelle con una vena di follia 😉

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.