Programmazione 0 commenti

Le avventure di un Pythonista in Schemeland /9

di Michele Simionato

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

Pubblicato il
28 Apr 2008
Tag

Commenti

Nessun commento.
Screencast e videocorsi di programmazione
Stacktrace RSS Feed Stacktrace via E-mail
Hai idee per un articolo? Faccelo sapere!