Le avventure di un Pythonista in Schemeland/8

Come promesso, in questa puntata parlerò di macro. Le macro di Scheme
sono particolarmente avanzate, di gran lunga più sofisticate di quelle
del Lisp e di qualunque altro linguaggio ed hanno una reputazione di
formidabile complessità. Per essere più precisi,
Scheme ha due sistemi di macro inclusi nello standard (più molti altri
sistemi più o meno diffusi): le macro basate su syntax-rules, che
sono di potere limitato ma relativamente semplici, e le macro basate su
syntax-case, che sono potentissime ma decisamente più complesse da
utilizzare. In questo puntata darò soltanto degli esempi di macro
basate su syntax-rules, assieme ad una minima discussione sui
pro e contra delle macro.
Prendete un bel respiro e allacciatevi le cinture che si parte!


Il buon vecchio ciclo for

La sintassi base di Scheme è volutamente molto spartana e come si è
detto nella quarta puntata il ciclo for non fa parte del linguaggio
perché non necessario in un linguaggio che garantisce
l’ottimizzazione di tail call. Supponiamo tuttavia di stare traducendo una
libreria da un altro linguaggio che ha il ciclo for e di voler fare il
minor sforzo possibile. Una delle caratteristiche peculiari di Scheme
e di tutti i linguaggi della famiglia del Lisp è che è possibile
estendere la loro sintassi a volontà tramite il meccanismo delle
macro. Questo significa che anche se il ciclo for non è builtin nel
linguaggio è possible inventarselo ed aggiungerlo alla sintassi del
linguaggio tramite una macro. Nella puntata precedente ho già mostrato un
semplice esempio di ciclo for che cicla su numeri interi con passo
unitario; non è difficile estenderlo ad un ciclo for che accetta
uno step generico, anche frazionario e/o negativo:

(define-syntax for
(syntax-rules (from to step) ; literals
((for i from start to end step s body ...)
(begin
(define cmp (if (> s 0) >= <=)); for positive step exit if i >= end
(assert (and (number? start) (number? end))); start and end must be numbers
(let loop ((i start))
(unless (cmp i end) body ... (loop (+ i s))))))
((for i from start to end body ...)
(for i from start to end step 1 body ...))))

Come vedete le macro di Scheme sono basate sul pattern matching.
In questo esempio for è il nome della macro che
stiamo definendo mentre syntax-rules (literal ...) è la funzione
di pattern matching che la definisce, detta anche il transformer
associato alla macro. I patterns riconosciuti da syntax-rules sono
della forma ((ignored arg ...) (template ...)) ... dove il primo argomento
viene ignorato dal compilatore, ma spesso per motivi di leggibilità viene
indicato con il nome della macro (for nel nostro caso). Altre volte si
usa un underscore _ per rendere chiaro che l’argomento è puramente
un placeholder.
In pratica stiamo istruendo il compilatore, dicendogli come deve
comportarsi quando vede certi pattern. In particolare, se vede
un’espressione for seguita da un indice di loop i, un separatore
from, un’espressione start, un separatore to, un’espressione
end e un separatore step seguito da un’espressione , ed infine
una serie di zero o più espressioni (il body) li deve convertire in un ciclo
named let opportuno, preceduto da un assert che garantisce che lo
step sia consistente. È possibile definire allo stesso tempo
più di un pattern ed usare pattern ricorsivi; in particolare qui
abbiamo reso lo step opzionale: se il compilatore non vede il
separatore step, deve assumere che lo step sia 1 e invocare il
pattern precedente.

Ecco un esempio d’uso un cui lo step non è specificato (e quindi vale 1):

> (for i from 1 to 4 (display i) (display " "))
1 2 3

Ecco un esempio con step uguale a 2:

> (for i from 0 to 6 step 2 (display i) (display " "))
0 2 4

Ecco un esempio con step negativo:

> (for i from 3 to 0 step -1 (display i) (display " "))
3 2 1

Ed eccone un altro, in cui start > end ed il loop non viene eseguito:

> (for i from 3 to 0 (display i) (display " "))

Infine, consideriamo un caso di errore un cui si passa come parametro
iniziale start qualcosa che non è un numero:

> (for i from 'a to 3 (display i) (display " "))
Unhandled exception
Condition components:
1. &assertion
2. &who: assert
3. &message: "assertion failed"
4. &irritants: ((and (number? 'a) (number? 3)))

Il messaggio di errore è chiaro, ma è anche poverissimo, visto che non dà
alcuna informazione sul numero di riga, né un traceback degno di
questo nome (qui sto usando Ikarus 0.0.2, versioni future
daranno qualche informazione in più).
La bontà dei messaggi di errore dipende
dall’implementazione che si sta usando, ma in generale anche le migliori
implementazioni di Scheme danno messaggi di errore piuttosto poveri, con
pochissima informazione rispetto al traceback di Python, soprattutto
quando si ha a che fare con le macro. Io non userei Scheme per
un’applicazione di livello enterprise: a parte il problema della
povertà dei messaggi di errore, tutto il linguaggio non è pensato per
l’uso industriale, ma per la ricerca e la sperimentazione.

Le macro come performance hack

La maggior parte di esempi di macro che si trovano nei tutorial per
principianti (compresi quello che ho appena dato) lasciano il tempo
che trovano, nel senso che danno sintassi alternative per fare
qualcosa che si può già fare usando la sintassi idiomatica di
Scheme. D’altra parte esistono situazioni in cui le macro fanno
davvero la differenza. Il punto importante da capire è che le macro
sono istruzioni per il compilatore ed in quanto tali sono espanse una
volta soltanto
, al momento della compilazione. Ci sono situazioni in
cui è possibile effettuare dei calcoli durante la compilazione e non a
runtime. Questo può avere dei vantaggi di performance non
indifferenti. Consideriamo per esempio la funzione di ordine
superiore call che abbiamo introdotto nella quarta puntata allo
scopo di usarla nei benchmark. Tale funzione ha uno svantaggio:
aggiunge una chiamata a funzione spuria ad ogni ciclo. Con una macro
è possibile evitare tale chiamata a funzione, che può inficiare i
risultati del benchmark. Ecco come call può essere rimpiazzata
con una macro repeat:

(library (repeat-macro)
(export repeat)
(import (rnrs))

(define-syntax repeat
(syntax-rules ()
((repeat n expr ...)
(let loop ((i 0))
(when (< i n) expr ... (loop (+ 1 i))))))))

Per verificare che la macro sia effettivamente più efficiente,
ho misurato il tempo necessario
a sommare 1+1 per dieci milioni di volte:

$ rlwrap ikarus
Ikarus Scheme version 0.0.2
Copyright (c) 2006-2007 Abdulaziz Ghuloum

> (import (repeat))
> (time (call 10000000 (lambda () (+ 1 1))))
running stats for (repeat 10000000 (lambda () (+ 1 1))):
no collections
189 ms elapsed cpu time, including 0 ms collecting
189 ms elapsed real time, including 0 ms collecting
40 bytes allocated
> (import (repeat-macro))
> (time (repeat-macro 10000000 (+ 1 1)))
running stats for (repeat 10000000 (+ 1 1)):
no collections
32 ms elapsed cpu time, including 0 ms collecting
33 ms elapsed real time, including 0 ms collecting
24 bytes allocated

Come vedete in questo esempio togliere la chiamata spuria fa una
differenza sostanziale: la stragrande maggioranza del tempo era spesa
nelle chiamate a funzione spurie, non nella somma! Anche l’occupazione
di memoria è dimezzata. Possiamo comunque verificare che l’uso di
call non inficia il benchmark sul fattoriale che abbiamo discusso
nella quinta puntata, in quanto il calcolo di un fattoriale è
un’operazione molto più onerosa della chiamata a funzione spuria,
quindi i risultati del benchmark non cambiano.

Le macro: pericolo o minaccia?

Il titolo di questo paragrafo è ironico e si riferisce al fatto che
nella comunità Python le macro sono solitamente malviste. Il motivo è
che l’esistenza delle macro per il programmatore industriale è più
spesso un malus che un bonus. Infatti spesso e
volentieri le macro vengono utilizzate per reinventare gli stessi
concetti con mille sintassi differenti e la manutenzioni di programmi
Scheme è notoriamente difficile. D’altra parte Paul Graham, un ben noto
estimatore delle macro scrive che

When there are patterns in source code, the response should not be to enshrine
them in a list of “best practices,” or to find an IDE that can generate them.
Patterns in your code mean you are doing something wrong. You should
write the macro that will generate them and call that instead.

Io sono d’accordo con la prima parte, ma non con la conclusione. È
vero che i pattern sono un code smell; d’altra parte la soluzione
per il programmatore industriale non è scriversi una macro ad uso
personale, ma avere qualcuno di fiducia (per esempio Guido van Rossum
nel caso di Python) che cambia il linguaggio per inglobare il pattern,
cosicché da beneficiare tutti gli utenti del linguaggio in maniera
centralizzata. Questo per esempio è avvenuto recentemente in Python
con il with statement. L’alternativa in cui ognuno si scrive il
proprio linguaggio ha senso per il ricercatore universitario o per
l’hacker solitario, o per gruppi di programmatori molto stretti, ma
non per la grande azienda. Naturalmente voi potete non essere d’accordo con
il mio punto di vista, ma secondo me per il programmatore industriale è molto
meglio un linguaggio senza macro in cui i costrutti utili sono già
stati tutti codificati. Dopotutto, nel 99.9% dei casi un programmatore
industriale risolve problemi che sono già stati risolti, non a caso
usa dei framework che fanno la maggior parte del lavoro di
infrastruttura. C’è solo un caso in cui si potrebbe arguire che le
macro hanno senso anche per un programmatore industriale: quando ci
sono problemi di performances. Tuttavia, anche in questo caso il
programmatore industriale (ho un mente un Pythonista/Rubysta) potrebbe
percorrere una via alternativa, passando al C/C++. Dopotutto esistono
varie tecniche di code generation per il C, mentre il C++ ha i
famigerati template: si tratta di soluzioni molto più povere
rispetto alle macro di Scheme, ma d’altra parte hanno il vantaggio
(enorme) di funzionare con linguaggi di uso comune cosa che
certamente Scheme non è.

Insomma, ci sono molte alternative possibili a Scheme per il programmatore
industriale. D’altra parte fare il programmatore industriale è molto spesso
noioso e il vostro cervello può rischiare di atrofizzarsi, mentre di certo
non correte questo rischio se continuate a leggere gli articoli di
Stacktrace ;) Considerate questa serie come una cura
contro la senilità precoce!

Una piccola challenge

Per solleticarvi l’ingegno vi lascio con
una challenge o sfida che dir si voglia: scrivere una macro che conta il numero dei suoi
argomenti. È abbastanza facile scrivere una macro che funziona per un
numero di argomenti fissato, tipo la seguente:

 (define-syntax count-args-up-to-three
(syntax-rules ()
((count-args-up-to-three) 0)
((count-args-up-to-three a1) 1)
((count-args-up-to-three a1 a2) 2)
((count-args-up-to-three a1 a2 a3) 3)))

> (count-args-up-to-three)

> (count-args-up-to-three 'some-arg)
1
> (count-args-up-to-three 'some-arg 'another-arg)
2
> (count-args-up-to-three 'some-arg another-arg 'third-arg)
3
disfida.jpg

tuttavia se chiamiamo count-args-up-to-three con quattro o più
argomenti otteniamo un errore di sintassi.
Come si fa a definire
una macro che funziona con un numero generico di argomenti?
Usando un trucco diabolico che svelerò soltanto
nella prossima puntata! Nel frattempo, vedete se riuscite a risolvere
la sfida senza imbrogliare, ovverossia senza andare a sbirciare un primer
su syntax-rules e senza usare eval o tecniche di
generazione del codice. Alla prossima e non mancate!

Comments

  1. Marco Benelli says:

    Complimenti per l’articolo.

    Confesso pero’ che il mio dissenso sul giudizio che dai dell’utilizzo di macro in ambito industriale non potrebbe essere piu’ grande :)

    Non considero “aver qualcuno di fiducia” che inglobi i pattern nel linguaggio un’alternativa alle macro. La ragion d’essere delle macro risiede nel fatto che gli autori di un linguaggio ad un certo punto si devono fermare.

    Van Rossum considera Python un esperimento sul migliore equilibrio tra potenza e facilita’ d’uso (correggetemi se ho parafrasato male). Mi sembra un attegiamento saggio: non tutti i pattern richiesti possono essere inclusi nel linguaggio (sopratutto se si vuol mantenere una certa semplicita’).

    E infatti tipicamente i pythonisti compensano all’assenza di macro con altre tecniche (quando possibile) o magari ricoronno al C++ con i famigerati template.

    D’altra parte mi sembra che anche i fatti ti smentiscano: molte applicazioni industriali sono realizzate in Common Lisp, spesso proprio per trarre vantaggio dal sistema di macro.
    E non dimentichiamo che il programma forse piu’ longevo della storia dell’informatica (Emacs) e’ realizzato con uso consistente di macro.

    In definitiva, penso che la critiche dei pythonisti nei confronti delle macro siano del tutto analoghe alle critiche rivolte all’eccessiva dinamicita’ di Python: certamente fondate, ma ingigantite da un pregiudizio, piuttosto che provate sul campo.

    Per concludere, vorrei notare che l’utillo di macro come performance hack e’ decisamente deprecato, ed appropriato a casi particolari (come quello che hai presentato tu). Lo stesso design di syntax-case e’ stato pensato per disincentivare l’abuso di macro:

    http://www.cs.indiana.edu/~chaynes/danfest/dyb.pdf

    video.google.com/videoplay?docid=-6899972066795135270&hl=en

    Chiedo scusa per la lunghezza di questo post, ma non ho avuto tempo di scriverne uno piu’ breve.

  2. Michele Simionato says:

    Bene, bene, sono contento che i lettori abbiamo
    le loro idee!
    Il motivo per cui io ho imparato Scheme e’ che anni
    fa c’e’ stato un dibattivo in c.l.py in cui dei lispers
    facevano il tuo stesso discorso (“il pregiudizio sulle macro e’ fondato sull’ignoranza”). A me non piace essere ignorante, volevo vedere chi aveva ragione, ho speso qualche migliaio di ore per imparare le macro e adesso penso che avessero ragione i Pythonisti ;) Comunque queste sono cose che dipendono dalla forma mentis del programmatore piu’ che da ogni altra cosa.
    Mi sorprende la tua frase
    “””
    E infatti tipicamente i pythonisti compensano all’assenza di macro con altre tecniche (quando possibile) o magari ricoronno al C++ con i famigerati template.
    “””
    Quali sono i casi secondo te in cui Python avrebbe bisogno delle macro? E il discorso sui templates del C++ mi pare fuori luogo (un programmatore Python programma in Python, non va a scriversi un ‘estensione C++ per poter usufruire dei template, a meno di non aver capito male il tuo argomento).

  3. Marco Benelli says:

    La mia posizione *non* e’: “il pregiudizio sulle macro e’ fondato sull’ignoranza”.

    Credo che, come ogni strumento potente, le macro vadano usate con giudizio, in questo senso ho detto che le critiche sono fondate.
    Usate saggiamente, le macro *aiutano* la manutebilita’ del codice, e ci sono molti esempi che lo dimostrano.

    Io non dico: “i pythonisti hanno torto”; se non vogliono aver a che fare con le macro possono avere le loro buone ragioni (cosi’ come i javisti possono avere le loro per non voler avere a che fare con il duck typing o l’operator overloading).

    E’ chiaro che se uno usa Python lo fa perche’ lo ritiene migliore, quindi capisco che un pythonista possa rimproverare java di essere troppo limitante e il lisp di esserlo troppo poco, ma la posizione: “per il programmatore industriale e’ meglio un linguaggio senza macro”, e’ molto piu’ forte, e’ come dire: “Java e’ usato solo da gente incapace” (che, del resto, e’ posizione comune tra i pythonisti).

    Per rispondere alla tua domanda: secondo me sono tantissimi i casi in vedo dei patterns python (e OOP in generale) che sarebbero meglio espressi tramite macro (*), e il riferimento al C++ e’ dovuto al fatto che l’hai citato come via alternativa nell’articolo.

    (*) inoltre puo’ interessare il punto 12 di :
    http://norvig.com/python-lisp.html
    in cui Norvig spende due parole sull’assenza di macro in python.

  4. francesco bracchi says:

    come il quadrato di flatlandia che guarda passare la sfera nel suo universo vede un cerchio che ingrandisce e rimpicciolisce, cosi il pythonista (questa volta) non vede la “tridimensionaltà” dell’universo dello schemedoka. Le macro consentono per prima cosa di mantenere nel cuore del linguaggio un insieme limitato di costrutti, spostando costrutti nuovi o avanzati in librerie.
    Quindi se un nuovo costrutto risulta utile all’intera comunità viene semplicemente reso pubblico o messo a parte delle libreria della distribuzione standard. Ma l’aspetto secondo me fondamentale è il metodo che le 2 civiltà hanno nell’affrontare un problema. Il Pythonista attacca il problema ficando gli elementi e trovando relazioni nel problema e della soluzione che andrà ad implementare, mentre lo schemedoka approccerà il problema linguisticamente, cercando di andare ad implementare un Domain Specific Language. Egli inizia morbidamente implementando un insieme limitato di costrutti e su questo fonda la sua soluzione. Se per il problema una descrizione in termini di oggetti fosse sufficiente, niente di più semplice che caricare la libreria di macro corrispondente, inoltre se un linguaggio non basta
    se ne possono aggiungere altri in uno stack di linguaggi (la famosa palla di fango).
    Quindi se il mio è un problema particolare (la cui soluzione non interessa l’intera comunità del linguaggio) non ha alcun senso aspettare che siano gli implementatori del linguaggio a costruire gli strumenti che mi permettano di risolverla.
    P.S: visto che molti scheme lo permettono nelle prossime puntate secondo me dovresti parlare anche delle macro stile lisp che permettono di vedere più chiaramente l’idea di code as data

  5. Michele Simionato says:

    Guarda, sono d’accordissimo con questa tua frase:
    “””se il mio è un problema particolare (la cui soluzione non interessa l’intera comunità del linguaggio) non ha alcun senso aspettare che siano gli implementatori del linguaggio a costruire gli strumenti che mi permettano di risolverla”””. Il mio punto era che i programmatori
    industriali il 99% delle volte hanno a che fare
    con problemi standard per cui a loro (per le loro
    tipologie di problemi) le macro NON convengono.
    Metto anche me stesso nei programmatori
    industriali e nella mia esperienza i vari framework
    (Web, GUI e che altro) coprono le mie esigenze
    lavorative senza che io senta il bisogno di macro.
    Il programmatore medio non ha bisogno normalmente
    di scriversi un DSL quindi non puo’ vedere il vantaggio
    delle macro.

  6. Marco Benelli says:

    Risolvere un problema tramite DSL e’ una scelta del programmatore piuttosto che un requisito del problema. Molto codice Python “industriale” che ho visto realizza DSL tramite gerarchie di classi, operator overloding…

  7. Giovanni Bajo says:

    Industrialmente non vengono usati i DSL perché non ci sono i linguaggi che lo rendono facile e di conseguenza manca la cultura. Quello che si fa è “simulare” un DSL magari tramite qualche magia nel costruttore, in una meta-classe, in un decoratore. Ma quello che “vorrei” è la possibilità di esprimere ad un livello più altro di quello che il linguaggio mi offre gli elementi che sto disegnando.

  8. Michele Simionato says:

    Al di la’ dei vari dibattiti, nessuno ha postato
    una soluzione alla sfida. Era difficile, per cui
    non mi aspettavo che la risolveste, a meno
    di non sapere gia’ il trucco risolutivo da qualche
    altro linguaggio funzionale, pero’ mi piacerebbe sapere se qualcuno almeno ci ha provato. Comunque scoprirete la soluzione molto presto perche’ la nona puntata e’ in dirittura d’arrivo …

  9. francesco bracchi says:

    ??
    (define-syntax count-args
    (syntax-rules ()
    ((_) 0)
    ((_ ?x ?xs …) (+ 1 (count-args ?xs …)))))

    (count-args 1 2 3 4 5) espande in
    (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 0)))))
    che si valuta in 5.

    tu dici “Il programmatore medio non ha bisogno normalmente di scriversi un DSL quindi non puo’ vedere il vantaggio delle macro”.
    Fermo restando che non è il programmatore medio quello che fa la differenza, l’uso delle macro è sintomo di affrontare il problema con un metodo diverso, che è quello appunto linguistico. è più una questione di testa (nel senso che ognuno ha la sua e ha percorsi mentali diversi e che ci fa preferire un linguaggio ad un altro perché corrisponde meglio a questi percorsi) e detto questo non è che un approccio sia meglio dell’altro. Inoltre concordo con quello che dice Bajo, circa la mancanza di cultura riguardo a questo metodo, ma l’unico modo è cominciare a costruirla.

  10. Michele Simionato says:

    Ma infatti uno degli scopi reconditi di queste serie e’ proprio quello di divulgare la cultura dei DSL! Nota
    pero’ che io non mi rivolgo ai programmatori medi …

  11. Pietro says:

    Io ci ho provato un po’ a tentoni, sono arrivato a questa conclusione:

    (define-syntax count-arguments
      (syntax-rules ()
        ((_ . arg) (length (quote arg)))))
    

    ma non sono sicuro che sia nello spirito dell’esercizio…

  12. Michele Simionato says:

    Non sara’ nello spirito dell’esercizio, pero’
    e’ una soluzione semplice e astuta che funziona:
    bravo! Io non ci avrei mai pensato!

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.