Le avventure di un Pythonista in Schemeland/10

exploding-head.jpg

In questo puntata chiudo il discorso sulle macro del secondo ordine
iniziato nella puntata scorsa, spiego i segreti dell’operatore di
ellipsis e mostro le soluzioni alle sfide da me lanciate. Inoltre,
spiego come definire una macro define-syntax+ che fornisce delle
funzionalità di introspezione e debugging alle macro che
definisce. Prima di cominciare a leggere, vi consiglio di prepararvi
una grossa scorta di caffè. Come si dice nel mondo Python, questa è
una puntata che rischia di farvi scoppiare la testa, quindi leggetela
con cautela e a vostro rischio e pericolo. Siete avvisati!


Il segreto dei puntini di sospensione

La parte delicata nella definizione della macro syntax-rules+ introdotta
nella scorsa puntata è la riga con i puntini di sospensione (ellipsis):

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

La sintassi (... ) è una notazione speciale che serve per fare
l’escaping dei puntini di sospensione; se x è un qualunque
template che non contiene l’ellipsis (... x) coincide con
l’identità: (... x) = x. Se però x contiene un ellipsis,
(... x) è un template in cui l’ellipsis viene trattato come un
identificatore letterale senza alcun significato particolare per il
compilatore (insomma, senza il significato di zero o più ripetizioni
dell’oggetto precedente). L’esistenza di (... ) è essenziale per
poter definire macro di ordine superiore. Se avessimo usato il
pattern più semplice

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

la nostra definizione di syntax-rules+ sarebbe stata
incapace di gestire macro contenenti ellipsis nei pattern. Considerate
per esempio la seguente definizione di una macro from:

(syntax-rules+ (upto)
 ((from i i0 upto i1 body ...)
  (let loop ((i i0))
   (unless (>= i i1) body ... (loop  (+ i 1))))))

Senza escaping dei puntini l’espansione di syntax-rules+ sarebbe stata

(syntax-rules (<literals> <patterns> <expand> upto)
  ((_ <literals>) '(upto))
  ((_ <patterns>) '((from i i0 upto i1 body ...)))
  ((from <expand> i i0 upto i1 body ...)
   '(let loop ((i i0))
      (unless (>= i i1) body ... (loop (+ i 1)))))
  ((from i i0 upto i1 body ...)
   (let loop ((i i0))
     (unless (>= i i1) body ... (loop (+ i 1))))))

Tale espansione dà luogo a codice sintatticamente scorretto, in quanto
i puntini di sospensione nella riga

((_ <patterns>) '((from i i0 upto i1 body ...)))

confondono il compilatore: se provate a eseguire la definizione di
from ottenete un errore di sintassi del tipo "misplaced ellipsis
in syntax form"
. La soluzione è utilizzare l’operatore (... ):
con la nostra definizione di syntax-rules+ la definizione
della macro from espande a

(syntax-rules (<patterns> <expand> upto)
  ((_ <literals>) '(upto))
  ((_ <patterns>) '((... (from i i0 upto i1 body ...))))
  ((from <expand> i i0 upto i1 body ...)
   '(let loop ((i i0))
      (unless (>= i i1) body ... (loop (+ i 1)))))
  ((from i i0 upto i1 body ...)
   (let loop ((i i0))
     (unless (>= i i1) body ... (loop (+ i 1))))))

che è grammaticalmente corretta in quanti i puntini in
((_ <patterns>) '((... (from i i0 upto i1 body ...))))
sono escaped. Tosto, eh?

Il segreto delle parentesi

Avendo discusso il segreto dei puntini di sospensione, veniamo all’altro
punto lasciato in sospeso nella puntata precedente: come definire una
condizionale con meno parentesi. La soluzione, come suggerito,
fa uso di un accumulatore e di un helper. Usando un trucco spiegato
nel Syntax-Rules Primer for the Merely Eccentric l’helper può
essere integrato nella macro stessa come segue:

(define-syntax if+
 (syntax-rules ()
  ((if+ "helper" (acc ...))
    (cond acc ...))
  ((if+ "helper" (acc ...) x)
    (syntax-violation 'if+ "Mismatched pairs" '(acc ... x) 'x))
  ((if+ "helper" (acc ...) x1 x2 x3 ...)
    (if+ "helper" (acc ... (x1 x2)) x3 ...))
  ((if+ x1 x2 ...)
    (if+ "helper" () x1 x2 ...))))

Il codice dovrebbe “parlare”. Il trucco consiste nel fare pattern matching
sulla stringa letterale "helper" per definire la macro ausiliaria
all’interno della macro principale (nulla vieta comunque di definirsi
la macro ausiliaria a parte).
Notate anche la gestione degli errori tramite syntax-violation:
se passate un numero dispari di argomenti a if+ ottenete un errore
di sintassi (cioè non un errore a runtime, ma a compile time).
Un esempio val più di mille parole:

> (let ((n 1))
    (if+ (= n 1) ; missing a clause
      (= n 2) 'two
      (= n 3) 'three
      else 'unknown))
Unhandled exception:
Condition components:
  1. &who: if+
  2. &message: "Mismatched pairs"
  3. &syntax:
      form: (((= n 1) (= n 2)) ('two (= n 3)) ('three else) 'unknown)
      subform: 'unknown

È anche possibile generalizzare il trucco di raggruppare coppie di valori, usando
una macro di supporto collecting-pairs:

(library (collecting-pairs)
(export collecting-pairs)
(import (rnrs))

(define-syntax collecting-pairs
  (syntax-rules ()
    ((collecting-pairs (name arg ...) x1 x2 ...)
     (collecting-pairs "helper" (name arg ...) () x1 x2 ...))
    ((collecting-pairs "helper" (name arg ...) (acc ...))
     (name arg ... acc ...))
    ((collecting-pairs "helper" (name arg ...) (acc ...) x)
     (syntax-violation 'name "Mismatched pairs" '(name arg ... acc ... x) 'x))
    ((collecting-pairs "helper" (name arg ...) (acc ...) x1 x2 x3 ...)
     (collecting-pairs "helper" (name arg ...) (acc ... (x1 x2)) x3 ...))
    ))
)

Notate che anche collecting-pairs è una macro del secondo ordine,
non nel senso che ritorna una macro, ma nel senso che prende una macro
come argomento.

Potete usare collecting-pairs per raggruppare a coppie gli argomenti
di espressioni sintattiche come cond, case, syntax-rules, eccetera.
Ecco un esempio d’uso con l’espressione case:

> (collecting-pairs (case 1)
      (1) 'one
      (2) 'two
      (3) 'three
      else 'unknown))
one

È una buona idea usare collecting-pairs ? E, più in generale, è
una buona idea escogitare trucchi per evitare le parentesi? Si tratta
di una questione sia filosofica che pratica. Probabilmente la
maggioranza dei programmatori trova più semplice scrivere codice con
meno parentesi. La filosofia di Scheme però è che è più
importante rendere semplice la generazione automatica di
codice piuttosto che la scrittura manuale di codice. Per dirla in chiaro, se state
scrivendo delle macro usando syntax-rules, è molto più semplice
usare la condizionale con più parentesi cond piuttosto che
if+. Il motivo è che le parentesi vi permettono di raggruppare le espressioni
in gruppi che possono essere ripetuti o meno tramite l’operatore di ellipsis: in
pratica potete scrivere cose tipo (cond (cnd? do-this ...) ...) che non potreste
scrivere con if+.

D’altra parte, linguaggi diversi adottano filosofie differenti. Per
esempio Arc di Paul Graham adotta la filosofia di usare meno
parentesi, ma può farlo perché non ha un sistema di macro basato sul
pattern matching (cosa che a mio parere è un grosso minus rispetto a
Scheme). È possibile salvare capra e cavoli? Ovverossia avere una
sintassi con poche parentesi quando si sta scrivendo codice ordinario
e molte quando si stanno scrivendo macro? La risposta è sì: basta
duplicare i costrutti base del linguaggio ed usare un approccio alla
Python, che fornisce sia una sintassi semplice che una di basso
livello: per esempio si potrebbe avere un __cond__ con molte
parentesi da usare nella macro ed un cond con meno parentesi da
usare normalmente. Questo in teoria: nella pratica però Scheme
fornisce soltanto la sintassi di basso livello e lascia all’utente
finale la libertà (o il carico che dir si voglia) di implementarsi la
sintassi di alto livello. Questo è fatto sia per motivi politici (è un
linguaggio disegnato da un comitato, è impossibile accordarsi su una
sintassi di alto livello che piaccia a tutti) che ideologici (alla
maggior parte dei programmatori Scheme va bene così, non amano le
imposizioni).

define-syntax+

Per finire, definiamo ora una macro di utilità, un define-syntax+
che estende il define-syntax di Scheme con una sintassi che
ha meno parentesi e maggiore similarità sintattica con il
defmacro del Common Lisp (per chi non conosce defmacro, la
migliore referenza sulle macro Lisp-style è il libro OnLisp di Paul Graham;
in una puntata futura discuteremo in dettaglio le differenze tra le macro
di Scheme e quelle del Common Lisp). define-syntax+ sarà molto
usato nella prossime puntate ed è definito come segue:

(define-syntax define-syntax+
  (syntax-rules+ ()
    ((define-syntax+ (name . args) templ)
     (define-syntax name (syntax-rules+ () ((name . args) templ))))
    ((define-syntax+ name (lit ...) (patt templ) ...)
     (define-syntax name (syntax-rules+ (lit ...) (patt templ) ...)))
    ((define-syntax+ name transformer); legacy syntax
     (define-syntax name transformer))
  ))

Come esempio d’uso, definiamo una macro dispatch-lambda che
ritorna una funzione che fa dispatch sul suo primo argomento:

(define-syntax+ (dispatch-lambda ((name . args) body1 body2 ...) ...)
  (let ()
    (define (name . args)
      body1 body2 ...)
    ...
    (lambda (n . a)
      (case n
        ((name) (apply name a)) ...
        (else (error 'dispatch-lambda (format "Unrecognized name ~a!" n)))
        ))))

Faremo uso di dispatch-lambda nella prossima puntata,
come punto di partenza su cui implementare un semplice sistema ad oggetti.
Qui ci serve soltanto come esempio per impratichirci con la macrologia
basata su define-syntax+. Per esempio, potreste volere vedere
l’espansione della macro in un caso particolare:

> (dispatch-lambda <expand> ((R) 'red) ((G) 'green) ((B) 'blue))
(let ()
  (define (R) 'red)
  (define (G) 'green)
  (define (B) 'blue)
  (lambda (n . a)
    (case n
      ((R) (apply R a))
      ((G) (apply G a))
      ((B) (apply B a))
      (else
       (error 'dispatch-lambda
         (format "Unrecognized name ~a!" n))))))

Per comodità ho raggruppato le macro definite in queste ultime puntate
in un modulo syntax-rules-plus che potete scaricare liberamente
(tutto il codice che accompagna questi articoli va inteso distribuito
con la licenza più liberale possibile, modificatelo a vostro
piacimento ma non chiedetermi i danni se qualcosa non funziona ;).

Con questo chiudo la puntata. Vi avverto però che non
abbiamo finito con le macro (in realtà abbia soltanto scalfito la
superficie dell’argomento, parlando soltanto delle macro più semplici,
quelle basate su syntax-rules) e che nel futuro lontano ci saranno delle
puntate che andranno a investigare le macro di tipo syntax-case
confrontandole con le macro tradizionali (Lisp-like) di tipo
define-macro. La prossima puntata invece sarà molto più pratica e
dedicata ad applicazioni concrete delle macro.

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.