Programmazione 12 commenti

Le avventure di un Pythonista in Schemeland/8

di Michele Simionato

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!

Pubblicato il
10 Apr 2008
Tag

Commenti

  • Marco Benelli il 11 Apr 2008

    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:

    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.

  • Michele Simionato il 12 Apr 2008

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

  • Marco Benelli il 13 Apr 2008

    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.

  • francesco bracchi il 15 Apr 2008

    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

  • Michele Simionato il 16 Apr 2008

    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.

  • Marco Benelli il 16 Apr 2008

    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...

  • Giovanni Bajo il 17 Apr 2008

    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.

  • Michele Simionato il 20 Apr 2008

    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 ...

  • francesco bracchi il 21 Apr 2008

    ?? (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.

  • Michele Simionato il 21 Apr 2008

    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 ...

  • Pietro il 21 Apr 2008

    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...

  • Michele Simionato il 21 Apr 2008

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

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