Programmazione 0 commenti

Le avventure di un Pythonista in Schemeland/10

di Michele Simionato
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.

Pubblicato il
8 Mag 2008
Tag

Commenti

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