Lisp – Costruzione di una semplice unità di test – 2

l4Da finire il thriller iniziato qui, copiando qui.

Visualizzare meglio i risultati

Se si ha solo una funzione da testare l’ultima versione, quella raccontata nel post precedente funziona. Ma se si devono scrivere parecchi test si può migliorare. Per esempio supponiamo di voler testare la funzione *. Si potrebbe pensare di scrivere:

(defun test-* ()
  (check
    (= (* 2 2) 4)
    (= (* 3 5) 15)))

Adesso che si hanno due funzioni di test conviene probabilmente scriverne un’altra che le esegue tutte.

(defun test-arithmetic ()
  (combine-results
   (test-+)
   (test-*)))

Questa funzione deve fare qualcosa così:

CL-USER> (test-arithmetic)
pass ... (= (+ 1 2) 3)
pass ... (= (+ 1 2 3) 6)
pass ... (= (+ -1 -3) -4)
pass ... (= (* 2 2) 4)
pass ... (= (* 3 5) 15)
T

cioè usare combine-results per avere i risultati parziali e globale sia per test-+ che per test-*.
Però se abbiamo diverse funzioni da esaminare ognuna con più casi sarebbe bene che il risultato dei fallimenti fosse più dettagliato.

Siccome il codice che scrive il risultato è centralizzato in report-result bisogna dire a questa che funzione si sta testando. Si potrebbe aggiungere un parametro a report-result ma check che genera le chiamate a report-result non sa da che funzione viene chiamata il che vorrebbe dire cambiare anche il modo con cui si chiama check, passandogli un argomento che passerebbe semplicemente a report-result.

Questo è il tipo di problema per cui esistono le variabili dinamiche. Se si crea una variabile dinamica che ogni funzione da testare collega al nome della funzione prima di chiamare check allora report-result può usarla anche se check non sa nulla a riguardo.

Il primo passo è di dichiarare la variabile al top level:

(defvar *test-name* nil)

Poi occorre modificare report-result per includere *test-name* nel format:

(format t "~:[FAIL~;pass~] ... ~a: ~a~%" result *test-name* form)

Con queste modifiche però la variabile *test-name* non è mai ridefinita e si avrebbe

CL-USER> (test-arithmetic)
pass ... NIL: (= (+ 1 2) 3)
pass ... NIL: (= (+ 1 2 3) 6)
pass ... NIL: (= (+ -1 -3) -4)
pass ... NIL: (= (* 2 2) 4)
pass ... NIL: (= (* 3 5) 15)
T

Occorre cioè inserirla in test-+ e test-* così:

(defun test-+ ()
  (let ((*test-name* 'test-+))
    (check
      (= (+ 1 2) 3)
      (= (+ 1 2 3) 6)
      (= (+ -1 -3) -4))))

(defun test-* ()
  (let ((*test-name* 'test-*))
    (check
      (= (* 2 2) 4)
      (= (* 3 5) 15))))

e quindi ottengo:

t9-5

Emerge un’astrazione

Aggiustando le funzioni di test abbiamo introdotto parecchie duplicazioni. Ogni funzione deve definire il nome 2 volte: in defun e nella variabile *test-name* ma le stesse righe di codice sono duplicate per le due funzioni. Già si potrebbe rimuoverle perché il codice non è bello ma c’è altro, una lezione importante da imparare su come si devono usare le macros, minaccia Peter, nèh, non io :wink:

La ragione per cui partono allo stesso modo è perché sono entrambe funzioni di test. La duplicazione c’è perché la funzione di test è solo la prima metà dell’astrazione che fa dire “questa è una funzione di test” ma il codice non lo esprime.

Le astrazioni parziali sono un povero tool (crummy dice Peter) per sviluppare codice. Intanto ci sono duplicazioni e poi il solito problema della manutenzione, specie con programmatori differenti, o lo stesso in tempi diversi. Insomma per rendere completa l’astrazione occorre che il modo di rendere “questa è una funzione di test” e avere il codice richiesto dal modello serve –indovina?– una macro.

Siccome lo schema che stiamo tentando di fare è di catturare una defun più qualche codice standard abbiamo bisogno di scrivere una macro che venga espansa in una defun. Si userà poi questa macro invece della della defun per definire le funzioni di test, quindi è sensato chiamarla deftest.

(defmacro deftest (name parameters &body body)
  `(defun ,name ,parameters
    (let ((*test-name* ',name))
      ,@body)))

Con questa macro si può riscrivere test-+ in questo modo:

(deftest test-+ ()
  (check
    (= (+ 1 2) 3)
    (= (+ 1 2 3) 6)
    (= (+ -1 -3) -4)))

Una gerarchia di test

Adesso salta fuori una domanda: test-arithmetic dev’essere una funzione di test? per come stanno le cose non ha importanza, se la si definisce con deftest il suo collegamento alla variabile *test-name* verrà showato (nascosto) da quelli collegati a test-+ e test-*.

Ma immaginiamo di avere migliaia di test da organizzare. Il primo livello dell’organizzazione è dato da test-+ e test-* che chiamano check direttamente. Ma con migliaia di casi probabilmente si renderanno necessari altri livelli di organizzazione. Funzioni come test-arithmetic possono raggruppare funzioni simili in pacchetti di test. Supponiamo che per qualche livello basso le funzioni siano chiamate da più pacchetti di test. E può capitare che un test passi in un contesto e fallisca in un altro. Se questo succede probabilmente serve conoscere più del solo nome della funzione di basso livello che contiene il test del caso.

Se si definisce il pacchetto di funzioni di test come test-arithmetic con deftest e si fa un piccolo cambiamento a *test-name* si possono avere i risultati riportati con tutto il percorso, qualcosa come:

pass ... (TEST-ARITHMETIC TEST-+): (= (+ 1 2) 3)

Siccome abbiamo già reso astratto il processo di definire le funzioni di test cambiare il risultato dell’output non richiede una modifica alle funzioni di test. Per rendere *test-name* contnente una lista di nomi invece di uno solo è richiesto solo di modificare la sua binding form da

(let ((*test-name* ',name))

a

(let ((*test-name* (append *test-name* (list ',name))))

append ritorna una nuova lista fatta dagli elementi dei sui argomenti. in questo caso aggiunge il nuovo nome alla lista attuale. Appendere con append non è il metodo più efficiente ma dai, in questo caso va più che bene. Quando la funzione di test ritorna il vecchio valore viene ripristinato.

Adesso si può ridefinire test-arithmetic con deftest invece che con defun:

(deftest test-arithmetic ()
  (combine-results
   (test-+)
   (test-*)))

E quando i test aumentano si potrà avere:

(deftest test-math ()
  (test-arithmetic))

Da provare!

t9-6

Conclusione

Questo modello è ampliabile, si possono aggiungere cose ma nella sua versione corrente è tutto qui (ho aggiunto with-gensym e le funzioni attuali di test, cioè tutti quello che serve per lanciare il test dello screenshot precedente):

(defmacro with-gensyms ((&rest names) &body body)
  `(let ,(loop for n in names collect `(,n (gensym)))
     ,@body))

(defvar *test-name* nil)

(defmacro deftest (name parameters &body body)
  "Define a test function. Within a test function we can call
   other test functions or use 'check' to run individual test
   cases."
  `(defun ,name ,parameters
    (let ((*test-name* (append *test-name* (list ',name))))
      ,@body)))

(defmacro check (&body forms)
  "Run each expression in 'forms' as a test case."
  `(combine-results
    ,@(loop for f in forms collect `(report-result ,f ',f))))

(defmacro combine-results (&body forms)
  "Combine the results (as booleans) of evaluating 'forms' in order."
  (with-gensyms (result)
    `(let ((,result t))
      ,@(loop for f in forms collect `(unless ,f (setf ,result nil)))
      ,result)))

(defun report-result (result form)
  "Report the results of a single test case. Called by 'check'."
  (format t "~:[FAIL~;pass~] ... ~a: ~a~%" result *test-name* form)
  result)  

(deftest test-+ ()
    (check
      (= (+ 1 2) 3)
      (= (+ 1 2 3) 6)
      (= (+ -1 -3) -4)))

(deftest test-* ()
    (check
      (= (* 2 2) 4)
      (= (* 3 5) 15)))    
      
(deftest test-arithmetic ()
  (combine-results
   (test-+)
   (test-*)))

E, cito il Peter:

It’s worth reviewing how you got here because it’s illustrative of how programming in Lisp often goes.

Siamo partiti da una semplice versione del problema –come valutare una serie di espressioni booleane simili e verificare che siano tutte verificate. Farne l’and era OK ma è saltata fuori la necessità di un report migliore per cui si è scritto qualche riga di codice super-semplice, pieno di duplicazioni e facilmente soggetto a errori.
La faccio breve, Peter è più didascalico, l’eliminazione delle duplicazioni ha tirato in ballo anche altro, e alla fine si è raggiunto un risultato con poche righe di codice, con un ttima astrazione, ampliabile. E sì, le macros, sempre loro. :mrgreen:

Lisp – Costruzione di una semplice unità di test – 1

l3Come da titolo, seguendo Peter Seibel (che rockz! assay).
Cosa che promette di usare alcune delle cose imparate finora.

Lo scopo principale dell’unità di test sarà di rendere il più possibile facile di aggiungere nuovi test per controllarne varie serie e tener conto di quelli non superati. partiamo con la progettazione di un’unità che si può usare durante lo sviluppo interattivo.

La caratteristica chiave di un’unità di test è che sia lei la responsabile di dirci se tutti i test sono passati. Non vogliamo perder tempo controllando l’output per vedere le risposte quando il computer può farlo più velocemente e accuratamente. Di conseguenza ogni test dev’essere un espressione booleana. per esempio se volessimo scrivere test per la funzione built-in + i casi potrebbero essere:

(= (+ 1 2) 3)
(= (+ 1 2 3) 6)
(= (+ -1 -3) -4)

Le funzioni che hanno side effects (effetti collaterali) devono essere testate in modo differente, verificando anche i previsti side effects. Ma in ogni caso vogliamo un SÌ o NO.

Due primi tentativi

Se si vogliono fare i test precedenti con la REPL basta verificare che ritornino t. Nel nostro caso si può scrivere una funzione che ritorna l’and di tutti i risultati:

t9-0

OK, finché torna t si sa che tutti sono passati. Ed è molto conciso. Ma appena uno fallisce ottieni nil ma non sai per quale caso.
Si potrebbe tentare un’altra via, qualcosa come questo:

t9-1

In questo caso l’output è migliore ma ancora grossolano. C’è la ripetizione di format e la duplicazione dell’espressione che si vuole testare, insomma riscrivere. Inoltre non si ottiene un risultato globale per tutta la serie e quando si hanno tanti test può capitare che non vedi il “FAIL”.

Refactoring

Come si dice da noi? Lascio quello.
Quel che si vuole è un risultato globale (t o nil) ma anche sui singoli test. Siccome la seconda versione è più vicina a quel che si vuole partiamo da quella, evitando le duplicazioni:

(defun report-result (result form)
  (format t "~:[FAIL~;pass~] ... ~a~%" result form))

Questa funzione si può usare dentro la test-+, in questo modo:

(defun test-+ ()
  (report-result (= (+ 1 2) 3) '(= (+ 1 2) 3))
  (report-result (= (+ 1 2 3) 6) '(= (+ 1 2 3) 6))
  (report-result (= (+ -1 -3) -4) '(= (+ -1 -3) -4)))

Adesso vediamo per la duplicazione. Quel che serve è di trattare l’espressione sia come codice (per avere il risultato) che come dati (per la scritta). Ogni volta che si vuole trattare il codice come dati saltano fuori le macros. Quello che vorremmo è qualcosa come:

(check (= (+ 1 2) 3))

con il significato di:

(report-result (= (+ 1 2) 3) '(= (+ 1 2) 3))

Scrivere la macro è banale (dice):

(defmacro check (form)
  `(report-result ,form ',form))

Nota per me: Uh! `|,|', :roll:

Adesso test-+ diventa:

(defun test-+ ()
  (check (= (+ 1 2) 3))
  (check (= (+ 1 2 3) 6))
  (check (= (+ -1 -3) -4)))

Possiamo evitare la ripetizione di tutte queste chiamate a check? Si può ridefinire check in modo da prendere un numero arbitrario di forms e avvolgerle tutte in una chiamata a report-result:

(defmacro check (&body forms)
  `(progn
     ,@(loop for f in forms collect `(report-result ,f ',f))))

progn esegue una serie di forms in sequenza (è il blocco {...} del C). Notare ,@ per connettere in una sola espressione la lista di espressioni generate dal modello backquotato.

con questa nuova versione di check test-+ può essere scritta:

(defun test-+ ()
  (check
    (= (+ 1 2) 3)
    (= (+ 1 2 3) 6)
    (= (+ -1 -3) -4)))

ed è equivalente a:

(defun test-+ ()
  (progn
    (report-result (= (+ 1 2) 3) '(= (+ 1 2) 3))
    (report-result (= (+ 1 2 3) 6) '(= (+ 1 2 3) 6))
    (report-result (= (+ -1 -3) -4) '(= (+ -1 -3) -4))))

grazie a check questa versione è concisa come quella iniziale ma espande il codice ottenendo la seconda versione.

t9-2

Aggiustare il valore di ritorno

Manca ancora il risultato globale.
Come primo passo si può fare una piccola modifica a report-result così che ritorni il risultato del caso che sta testando

(defun report-result (result form)
  (format t "~:[FAIL~;pass~] ... ~a~%" result form)
  result)

Adesso sembrerebbe che basti un progn e un and per combinare i risultati. Purtroppo no, and non funziona per via del comportamento short-circuit, si fermerebbe al primo nil e salterebbe i test successivi. Common Lisp non fornisce quello che vogliamo ma ci sono le macros :wink:

Lasciamo da parte i test per vedere come fare la macro, chiamiamola combine-results:

(combine-results
  (foo)
  (bar)
  (baz))

avendo in mente qualcosa simile a:

(let ((result t))
  (unless (foo) (setf result nil))
  (unless (bar) (setf result nil))
  (unless (baz) (setf result nil))
  result)

L’unica cosa è che bisogna inserire la variabile result nell’espansione. Come visto precedentemente usare un nome letterale per una variabile può produrre leak nell’espansione della macro. Così occorre creare un nome unico, lavoro per with-gensym. Per comodità la riporto qui, metti che uno non l’ha presente :wink:

(defmacro with-gensyms ((&rest names) &body body)
  `(let ,(loop for n in names collect `(,n (gensym)))
     ,@body))

Si può così definire combine-results in questo modo:

(defmacro combine-results (&body forms)
  (with-gensyms (result)
    `(let ((,result t))
      ,@(loop for f in forms collect `(unless ,f (setf ,result nil)))
      ,result)))

e quindi aggiustare check semplicemente usando combine-results invece di progn:

(defmacro check (&body forms)
  `(combine-results
    ,@(loop for f in forms collect `(report-result ,f ',f))))

Con queste modifiche ottengo:

t9-3

E nel caso in cui uno (o più) test falliscano si ottiene nil:

t9-4

(continua, prossimamente) :mrgreen:

Lisp – Macros, definirne di nostre – 3

l2
Si continua, come al solito copiando da qui.
Post complicato oggi, di quelli che poi ti tornano di notte… Avvisati, nèh!

Macros che scrivono macros

Se possiamo scrivere macros che scrivono funzioni, inserendo astrazioni nei compiti comuni allora perché non approfittarne in pieno e applicare lo stesso procedimento anche per le macros?
Nota personale: ecco, quando 30+ anni fa ho letto GEB di DRH mi son detto “WOW, UAU” anche per questo; lavoravo in Fortran al tempo.

Seguendo il Peter (devo comprare il suo libro di carta, usarlo come testo sacro) scriverò una macro gensym-osa che scrive una macro:

[…] a macro that generates code that generates code. While complex macro-writing macros can be a bit confusing until you get used to keeping the various levels of code clear in your mind, with-gensyms is fairly straightforward and will serve as a useful but not too strenuous mental limbering exercise.

Ecco, proprio come da nota precedente. Sto ringiovanendo :grin:

Vogliamo scrivere qualcosa come questo:

(defmacro do-primes ((var start end) &body body)
  (with-gensyms (ending-value-name)
    `(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
          (,ending-value-name ,end))
         ((> ,var ,ending-value-name))
       ,@body)))

e renderla equivalente alla precedente versione di do-primes. In altre parole con with-gensym dobbiamo espandere in un let ogni variabile, ending-value-name in questo caso in un simbolo gensym-ico. Questo è semplice con un semplice modello di backquote.

(defmacro with-gensyms ((&rest names) &body body)
  `(let ,(loop for n in names collect `(,n (gensym)))
     ,@body))

Notare l’uso della virgola per interpolare il valore nell’espressione di loop.
Il ciclo genera una lista di binding forms ognuna delle quali consiste in una lista contenente uno dei nomi dati da with-gensym e dal codice letterale (gensym).
Si può testare questo codice il loop genera sostituendo i nomi con una lista di simboli:

l8-4

Dopo la lista delle binding forms il corpo di with-gensym è inserito nel corpo di let. Allora nel codice che costituisce la with-gensym ci si può riferire alle variabili nominate nella lista di variabili passate da with-gensym.

Espandendo la form di with-gensym nella nuova definizione di do-primes risulta qualcosa del genere:

(let ((ending-value-name (gensym)))
  `(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
        (,ending-value-name ,end))
       ((> ,var ,ending-value-name))
     ,@body))

Sembra buona (cit.). Anche se sembra triviale è importante aver chiaro come le differenti macros sono espanse: quando si compila defmacro per do-primes la form with-gensym è espansa nel codice appena visto e compilato. Quindi la versione compilata di do-primes è esattamente la stessa che avevamo scritto a mano con il let esterno. Quando si compila una funzione che usa do-primes il codice generato da with-gensym gira generando l’espansione di do-primes ma la with-gensym stessa non è necessaria per compilare la form di do-primes perché è già stata compilata quando la do-primes venne compilata.
Continuando la nota di prima: mai detto che DHR sia facile :roll:

Un’altra classica macro-writing macro: once-only

Un’altra macro-writing macro classica è once-only che è usata per generare codice che valuta certi argomenti di una macro una volta sola e in un ordine particolare. Usando once-only si può scrivere do-primes quasi semplicemente come la prima versione leaky, così:

(defmacro do-primes ((var start end) &body body)
  (once-only (start end)
    `(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
         ((> ,var ,end))
       ,@body)))

Tuttavia –panico, almost– l’implementazione di once-only è –come dire, panicosassay– perché consiste in livelli multipli di backquoting e unquoting. Dice il Peter: ” If you really want to sharpen your macro chops, you can try to figure out how it works“. Comunque eccola:

(defmacro once-only ((&rest names) &body body)
  (let ((gensyms (loop for n in names collect (gensym))))
    `(let (,@(loop for g in gensyms collect `(,g (gensym))))
      `(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
        ,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))
           ,@body)))))

Altra nota: once-only è famosa, provate a googlare e vedrete. per esempio: Understanding how to implement once-only lisp macro,
oppure “Any concise description would be far too vague to grasp, but…“, qui, dove conclude con “The description here is frustratingly non-descriptive, and I apologize for that. If you understand once-only and can give a better explanation, I would be very grateful—not to mention completely awed“.

Oltre alle macros semplici

Ci sarebbe parecchio altro da dire sulle macros. Quello visto è solo robetta semplice, esempi che permettono di risparmiare un po’ di codice. Prossimamente ci saranno esempi di cose che senza senza le macros sarebbero impossibili.
Ad esempio no, pausa :mrgreen:

Contro VisiCalc e derivati – I

sed-awkPrima ancora di cominciare due comunicazioni urgenti:

E poi anche se è un rant o una lamentela piagnucolosa sono io che sbaglio. The show must ‘ndé nans! nèh. :wink:

Allora vi racconto una storia tragica, o comica, o –OK, mia.

xls

Una mia carissima amica mi chiede di prendere i dati in colonna A e da questi estrarre le tre sottostringhe, quelle delle colonne B, C e D. Fatto, non è stato difficile, Excel –o affini, io ho usato Calc il modulo di Libre Office– ha in se un linguaggio di programmazione che si può usare.
Tutto sta a capacitarsi che invece di if devi scrivere SE e VAL.ERRORE e STRINGA.ESTRAI e simili. E poi fare riferimento alla celle. Fatta una riga poi copincolli e fatto. Quasi.
La mia soluzione (non ci ho messo molto, anzi) è quella in figura. Non si vede ma le colonne dalla B alla G sono tutte formule. Ecco, la pecca è che io mi sono preso la libertà di memorizzare i risultati intermedi nelle colonne E, F e G.
Ma sapete, ormai la programmazione funzionale colpisce anche dove non te lo aspetteresti! Anche i discendenti di VisiCalc!

Si può, certo che si può. Solo che a me viene male solo a pensarci. Ci ho provato per un po’, salvando continuamente lo step a cui ero arrivato e che stavo per distruggere (sono maldestro come pochi, davvero).

Poi ho finito per rinunciarci. Tre colonne in più non sono la fine del foglio! O forse sì ma è una cosa che davvero non ce la faccio. M’impappino con i SE VAL… e appena tenti di scrivere il valore di una cella ti mette a tradimento una coppia di parentesi che manco fossimo in Lisp. E anche a scrivere la formula in un editor normale e poi pastarla nella cella –insomma non ci sono riuscito :evil:

E in tutto questo tempo non potevo non pensare a come si sarebbe fatto una volta. Io probabilmente avrei usato AWK, ma forse invece sed. O qualcos’altro.
La prima volta che proposi al mio capo, molto tempo fa, nello scorso millennio, di usare AWK ricordo perfettamente cosa mi disse: “No, funzionerebbe, se funzionasse, solo con Unix“.

OK, ho promesso di non lamentarmi e non lo farò. Ma dovete ammettere che il file di testo con i codici sarebbe molto più piccolo e maneggevole, che lo script sarebbe di poche righe e che…
OK, largo ai giovani :mrgreen:

Lisp – Macros, definirne di nostre – 2

l1Si continua da qui, seguendo l’ottima guida, qui.

Tappare le perdite

L’espressione colorita la dobbiamo a Joel Spolsky, rende l’idea, e non c’è astrazione perfetta ma non bisogna tollerare quelle facilmente eliminabili.
Salta fuori che una macro può perdere dettagli in tre modi che però sono facilmente individuabili e correggibili.
La macro do-primes, definita nel post precedente, ne ha uno: valuta la subform end troppe volte. Se invece di chiamarla con un numero, 19 nell’esempio precedente, viene chiamata con un espressione, p.es. (random 100), così:

(do-primes (p 0 (random 100))
  (format t "~d " p))

si vede che non fa quello che deve, usiamo macroexpand-1:

l8-3

Quando il codice espanso gira random verrà chiamata ogni volta che viene valutata la condizione di fine del test. E quindi invece di testare se p è maggiore di un dato valore iniziale viene considerato un valore casuale che cambia per ogni iterazione.

Un rimedio potrebbe essere di definire questo come un comportamento di do-primes –una schifezza che va contro al Principle of Least Astonishment, principio di stupore minimo, di implementazione delle macros.
E i programmatori si aspettano che le forms passate alle macros siano valutate sono per quanto necessario. Inoltre do-primes è costruita sul modello di dotimes e dolist dove questo leak non c’è e quindi non dev’esserci neanche in do-primes.

Si può correggere facilmente; basta eseguire end una sola volta salvando il risultato in una variabile che verrà successivamente utilizzata. Ricordando che nel ciclo do le variabili vengono definite con una form di inizializzazione si può scrivere:

(defmacro do-primes ((var start end) &body body)
  `(do ((ending-value ,end)
        (,var (next-prime ,start) (next-prime (1+ ,var))))
       ((> ,var ending-value))
     ,@body))

Atz! questo introduce 2 nuovi leaks!
Uno è simile a quello corretto. Siccome le forms di inizializzazione per le variabili in un ciclo do sono valutate nell’ordine con cui le variabili sono definite quando l’espansione della macro è valutata l’espressione passata come end viene valutata come start, opposta all’ordine con cui appare nella chiamata alla macro. Quando si hanno valori come 0 e 19 non ci sono problemi ma con le forms si possono avere effetti collaterali e valutandole fuori ordine si viola il Principle of Least Astonishment.

La soluzione è di scambiare l’ordine delle valutazioni delle due variabili:

(defmacro do-primes ((var start end) &body body)
  `(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
        (ending-value ,end))
       ((> ,var ending-value))
     ,@body))

L’altro leak è stato creato usando come nome di variabile endig-value. Questo nome che sembra solo un dettaglio interno dell’implementazione della macro può finire per interagire con il codice passato alla macro o nel contesto in cui la macro è chiamata. Nella seguente chiamata a do-primes sembra tutto a posto ma non funziona come ci si aspetta, a causa di questo leak:

(do-primes (ending-value 0 10)
  (print ending-value))

e neanche questa:

(let ((ending-value 0))
  (do-primes (p 0 10)
    (incf ending-value p))
  ending-value)

Al solito macroexpand-1 mostra il problema, la prima chiamata diventa:

(do ((ending-value (next-prime 0) (next-prime (1+ ending-value)))
     (ending-value 10))
    ((> ending-value ending-value))
  (print ending-value))

Qualche implementazione del Lisp può non accettare che ending-value sua usato sia come nome di variabile nel ciclo do. Se invece passa il codice gira per sempre siccome ending-value non può essere maggiore di se stesso.

Il secondo problema viene espando così:

(let ((ending-value 0))
  (do ((p (next-prime 0) (next-prime (1+ p)))
       (ending-value 10))
      ((> p ending-value))
    (incf ending-value p))
  ending-value)

In questo caso il codice è perfettamente legale ma non va perché il collegamento stabilito da let per ending-value è esterno al ciclo e viene shadoved dalla variabile con lo stesso nome nel do, la form (incf ending-value p) incrementa la variabile nel ciclo invee di quella esterna con lo stesso nome, creando un altro ciclo infinito.

Risulta chiaro che quel che serve per toppare questo leak è di usare un simbolo che non verrà mai usato all’esterno della macro. Si può tentare con un nome improbabile ma non c’è garanzia. Si potrebbero usare i packages (non ancora visti). Ma c’è una soluzione migliore.

La funzione gensym ritorna un simbolo unico ogni volta che viene chiamata. Quindi invece del nome ending-value si può generare un nuovo simbolo ogni volta che do-primes è espansa.

defmacro do-primes ((var start end) &body body)
  (let ((ending-value-name (gensym)))
    `(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
          (,ending-value-name ,end))
         ((> ,var ,ending-value-name))
       ,@body)))

Notare che il codice che chiama gensym non fa parte dell’espansione; gira come parte del macro expander e quindi crea un nuovo simbolo ogni volta che la macro è espansa. Può sembrare strano che ending-value-name sia il nome di un’altra variabile, non diverso dal parametro var che è il nome di una variabile. la differenza è che il valore di var viene creato dal reader quando la macro viene letta mentre il valore di ending-value-name viene generato programmaticamente quando il codice della macro gira.

Con questa definizione tutto funziona, La prima form:

(do-primes (ending-value 0 10)
  (print ending-value))

espande in:

(do ((ending-value (next-prime 0) (next-prime (1+ ending-value)))
     (#:g2141 10))
    ((> ending-value #:g2141))
  (print ending-value))

Adesso la variabile usata per il valore finale è il simbolo generato #:g2141. Questo nome è stato generato da gensym ma non è importante; quello che importa è l’identità dell’oggetto del simbolo. I simboli generati da gensym sono scritti con il prefisso #:.

L’altra form problematica

(let ((ending-value 0))
  (do-primes (p 0 10)
    (incf ending-value p))
  ending-value)

viene così espansa:

(let ((ending-value 0))
  (do ((p (next-prime 0) (next-prime (1+ p)))
       (#:g2140 10))
      ((> p #:g2140))
    (incf ending-value p))
  ending-value)

E non c’è più il leak perché la variabile creata dal let non è più mascherata da una introdotta dall’espansione.

Non tutti i nomi letterali usati nella macro expansion causano necessariamente problemi –cosa che quando si diventa esperti si riesce a vedere– ma non c’è ragione per non gensym-are per sicurezza.

Con questo aggiustamento si sono tappate tutte le perdite di do-primes. Dopo aver scritto un po’ di macro si scopre che è facile a scriverle senza questi leak se si seguono queste semplici regole:

  • se non ci sono particolari ragioni per fare diversamente, includere ogni subforms nell’espansione nella posizione che sarà valutata nello stesso ordine in cui appare  nella chiamata alla macro;
  • se non ci sono particolari ragioni per fare diversamente, assicurarsi ce le subforms siano valutate una volta sola creando una variabile nell’espansione per contenere il valore della valutazione della form e usare questa variabile ovunque il valore dell’espansione è richiesto;
  • usare gensym nell’espansione della macro per creare nomi di variabile usate nell’espansione.

OK, adesso –no pausa :mrgreen:

Lisp – Macros, definirne di nostre – 1

l0Adesso, dice Peter, è ora di definire le nostre macro. Panico :roll: no, ormai dai :grin:. La difficoltà principale di capire le macro è forse che sono troppo integrate nel linguaggio. Ma Peter non si stanca di ripetere che “macros operate at a different level than functions and create a totally different kind of abstraction“. E ancora:

Once you understand the difference between macros and functions, the tight integration of macros in the language will be a huge benefit. But in the meantime, it’s a frequent source of confusion for new Lispers. The following story, while not true in a historical or technical sense, tries to alleviate the confusion by giving you a way to think about how macros work.

La storia di Mac, solo una storia

Premessa mia: Mac non è quello là; è una delle prime versioni al MIT, forse vuol dire Men And Computers, forse qualcos’altro. Allora non c’erano le macro e si doveva riscrivere e a quei tempi anche il copiancolla e –OK, la storiella è divertente, Peter la racconta benissimo, leggetela da lui. Ha anche un bel finale :lol:

Macro Expansion Time vs. Runtime

Il segreto di capire le macro è di aver chiara la distinzione quando il codice che genera codice (quello della macro) e il codice che finisce per diventare il programma (tutto il resto). Quando si scrive una macro si scrive codice che sarà usato dal compilatore per generare codice che sarà compilato. Solo dopo che tutte le macros sono espanse e il codice risultante è compilato il programma può girare. Il momento in cui le macros girano è chiamato macro expansion time; è distinto dal runtime quello in cui il codice –incluso quello generato dalle macro– gira. La distinzione è importante –e facile, la do per scontata :roll: Supponiamo che nel programma ci sia:

(defun foo (x)
  (when (> x 10) (print 'big)))

Normalmente viene da pensare a x come una variabile con un valore che viene passato a foo. Ma quando la macro viene espansa, vale a dire quando il compilatore fa girare la macro when, è disponibile solo il codice sorgente. Siccome il programma non sta girando non c’è nessun valore associato a x. Supponiamo che when sia (come abbiamo già visto) così definita:

(defmacro when (condition &rest body)
  `(if ,condition (progn ,@body)))

Quando il codice viene compilato la macro when viene eseguita con queste due forms come argomenti. Il parametro condition viene collegato a (> x 10) e la form (print 'big) viene raccolta in una lista che diventa il valore del corpo del parametro &rest. La backquote genererà questo codice: (if (> x 10) (progn (print 'big))) interpolando tra il valore di condition e unendo i valori del corpo con progn. Quando il Lisp è interpretato, invece di compilato, la distinzione tra i tempi di espansione delle macros e quello di runtime è meno chiara perché collegate. Inoltre il linguaggio non specifica come devono essere trattate le macro ma in ogni caso who cares come direbbero i west-padagni indigeni locali di qui :wink:

defmacro

Le macros vengono definite con defmacro che sta per DEFine MACRO; lo schema base è simile a quello di defun:

(defmacro name (parameter*)
  "Optional documentation string."
  body-form*)

Siccome le macros possono usare tutto quanto il Lisp qui ce ne sarà solo un pochino ma verrà descritto il processo per scrivere macros, dalla più semplice alla più complessa. Il compito di una macro è di tradurre una macro form in codice che fa una particolare cosa. Normalmente si parte scrivendo il codice che si vuole scrivere, cioè con un esempio della macro form. Altre volte si decide di scrivere la macro dopo aver scritto più volte lo stesso codice e si capisce che è il momento di astrarre. In ogni caso devi sapere sia da dove vieni e dove vuoi arrivare prima di sperare di scrivere il codice che lo fa automaticamente. Allora il primo passo è di scrivere almeno un esempio di una chiamata alla macro e di come sarà espansa. Quando si ha l’esempio di chiamata e di come viene espansa si passa al secondo step: scrivere il codice della macro. Per una macro semplice questo si riduce a scrivere un backquoted template con i parametri inseriti nel loro posto. Le macros complesse sono molto più impegnative. :evil: Dopo aver scritto il codice per tradurre la chiamata d’esempio nell’espansione appropriata ti devi assicurare che l’astrazione non perda (leak) dettagli nella sua implementazione. Con un’astrazione leaky la macro funziona con certi argomenti ma con altri interagisce con l’ambiente in modo indesiderato. Finisce che le macros possono perdere in diversi modi, tutti che possono essere facilmente evitati se si sa come controllarne la presenza. Cosa che verrà discussa in “Tappare le perdite” Nota: detesto queste metafore da idraulico, non sono Mario. Riassumendo, i passi per scrivere una macro sono:

  • scrivere una semplice chiamata e il codice che dev’essere espando, o vice versa;
  • scrivere il codice che genera l’espansione scritta a mano nell’esempio di chiamata;
  • assicurarsi che l’astrazione della macro non perda.

Una macro d’esempio: do-primes

Per vedere come i tre passi enunciati vengono messi in pratica scriviamo la macro do-primes che con un ciclo similare a dotimes e dolist eccetto che invece di iterare con interi o elementi di una lista itera per numeri primi successivi. Non è una macro particolarmente utile, solo un esempio per vedere come si fa. Per prima cosa servono due funzioni d’utilità, una che ci dice se un numero è primo e un’altra che ci ritorna il primo successivo maggiore o uguale ai suoi argomenti. In entrambi i casi si può ricorrere all’inefficiente forza bruta:

(defun primep (number)
  (when (> number 1)
    (loop for fac from 2 to (isqrt number) never (zerop (mod number fac)))))

(defun next-prime (number)
  (loop for n from number when (primep n) return n))

Nota mia: primep andrebbe esaminata attentamente :roll: Adesso si può scrivere la macro. Seguendo la procedura definita prima ci serve un esempio di chiamata e di espansione. Ecco:

(do-primes (p 0 19)
  (format t "~d " p))

per esprimere un ciclo che esegue il corpo una volta per ognuno dei primi maggiori o uguali a 0 e minori o uguali a 19, con la variabile p che contiene il numero primo. Conviene seguire il modello delle macro dotimes e dolist; le macros che seguono un modello di macros esistenti sono più facili da capire di quelle che introducono una sintassi nuova. Senza la macro do-primes si potrebbe scrivere un ciclo con do e le due utility definite prima come questo:

(do ((p (next-prime 0) (next-prime (1+ p))))
    ((> p 19))
  (format t "~d " p))

Siamo adesso pronti per scrivere il codice che traduce dal precedente a quest’ultimo.

Parametri delle macros

Siccome gli argomenti passati a una macro sono oggetti Lisp rappresentanti il codice sorgente della chiamata il primo passo per ogni macro è di estrarre quali sono le parti che servono per calcolare l’espansione. Per macros che interpolano semplicemente i loro argomenti in un template questo passo è banale: definire i parametri giusti per contenere i differenti argomenti è sufficiente. Ma questo approccio non sembra essere sufficiente per do-primes. Il primo argomento nella chiamata a do-primes è una lista contenente il nome della variabile del ciclo, p; il limite inferiore, 0; e il limite superiore, 19. Ma guardando all’espansione la lista come tale non appare nell’espansione; i tre elementi sono divisi e messi in posti diversi. Si può definire do-primes con due parametri, uno per contenere il primo e un parametro &rest per le forms del corpo, e quindi dividere la lista, in qualcosa di simile:

(defmacro do-primes (var-and-range &rest body)
  (let ((var (first var-and-range))
        (start (second var-and-range))
        (end (third var-and-range)))
    `(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
         ((> ,var ,end))
       ,@body)))

Tra un attimo la spiegazione di come il corpo generi l’espansione corretta; per adesso si noti come var, start e end hanno ognuna il valore, estratto da var-and-range, che viene interpolato nella backquote che genera l’espansione di do-primes. Tuttavia non è necessario dividere var-and-range a mano perché le liste dei parametri delle macro sono quelle che vengono chiamate destructuring parameter lists. Come il nome suggerisce involvono il suddividere la struttura, in questo caso la lista delle forms passate alla macro. Con una destructuring parameter list un semplice parametro può essere rimpiazzato in una lista di parametri annidata. I parametri nella lista di parametri annidata prendono i loro valori dagli elementi dell’espressione che sarebbero stati collegati nella lista rimpiazzata. Per dire si può rimpiazzare var-and-range con la lista (var start end) e i tre elementi verranno automaticamente destrutturati in questi tre parametri. Un’altra caratteristica speciale delle macro parameter lists è che si può usare &body come sinonimo di &rest. Semanticamente &body e &rest sono equivalenti ma diversi ambienti usano &body per modificare l’indentazione usata per le macro. Così si può snellire la definizione di do-primes e dare un suggerimento sia al programmatore che legge sia al tool di sviluppo definendo la macro così:

(defmacro do-primes ((var start end) &body body)
  `(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
       ((> ,var ,end))
     ,@body))

Oltre che essere più concisa la destructuring parameter lists esegue un controllo automatico degli errori. Con do-primes definito in questo modo Lisp sarà capace di trovare le chiamate in cui il primo argomento non è una lista di tre elementi e dare un messaggio d’errore chiaro di chiamata a funzione con troppi pochi argomenti. Inoltre se si unsa SLIME (Emacs) che i indica quali argomenti ci si aspettano appena scrivi il nome della funzione o macro con la destructuring parameter lists sarà in grado di essere più esplicito della sintassi della chiamata alla macro. Con la definizione originale SLIME direbbe che do-primes è chiamata come: (do-primes var-and-range &rest body) ma con la nuova definizione può irti che dev’essere così: (do-primes (var start end) &body body) Le destructuring parameter lists possono contenere parametri &optional, &key e &rest e destructuring parameter lists annidate. Cose non richieste nel nostro esempio.

Generare l’espansione

Siccome do-primes è semplice e abbiamo destrutturato gli argomenti resta solo da interpolarli nel template per avere l’espansione. Per le macro semplici la sintassi di backquote è perfetta. Ci ricordiamo vero che un’espressione backquotata è come una quotata tranne che si può squotare una subespressione precedendola con una virgola e per le liste un ,@. Un altro modo di vedere la backquote è come metodo conciso di scrivere codice che genera liste. Questo è esattamente quel che succede quando il reader legge l’espressione backquotata, la traduce nel codice che genera la struttura appropriata. Per esempio `(,a b) può essere letta come (list a 'b). Lo standard del linguaggio non specifica cosa il reader deve produrre. Nota: capito poco. Ecco una tabella con alcuni esempi

Backquote Syntax    Equivalent List-Building Code  Result
`(a (+ 1 2) c)	    (list 'a '(+ 1 2) 'c)	   (a (+ 1 2) c)
`(a ,(+ 1 2) c)	    (list 'a (+ 1 2) 'c)	   (a 3 c)
`(a (list 1 2) c)   (list 'a '(list 1 2) 'c)	   (a (list 1 2) c)
`(a ,(list 1 2) c)  (list 'a (list 1 2) 'c)	   (a (1 2) c)
`(a ,@(list 1 2) c) (append (list 'a) (list 1 2)   (list 'c)) (a 1 2 c)

È importante notare che la backquote è solo una convenienza ma ne è una grossa. Per apprezzarla compariamo la versione backquotata di do-primes data sopra con quella che usa le liste esplicite:

(defmacro do-primes-a ((var start end) &body body)
  (append '(do)
          (list  (list (list var
                             (list 'next-prime start)
                             (list 'next-prime (list '1+ var)))))
          (list (list (list '> var end)))
          body))

Vedremo prossimamente che l’implementazione corrente di do-primes non gestisce correttamente un certo caso limite. Ma per iniziare vediamo che funzioni per l’esempio originale. si può fare il test in due modi. si può provare se va, ecco: l8-0 OK. Oppure si può controllare la macro direttamente nella sua espansione di una particolare chiamata. La funzione macroexpand-1 prende una qualsiasi espressione Lisp come argomento e ritorna il risultato di un livello di espansione di macro. C’è anche la funzione macroexpand che continua a espandere ma visualizza molti più dettagli a basso livello, panicosa :wink: Poiché macroexpand-1 è una funzione per passargli un argomento letterale bisogna quotarlo. Provo: l8-1 OK. Se si usasse SLIME (mi sa che prima o poi…) poni il cursore nella parentesi aprente la fporm della macro e digiti C-c RET e il risultato è stampato in un buffer. In ogni caso sembra che tutto funzioni. Ma … –prossimamente :mrgreen:

Monkeys

monkey_faceUn post non tanto serio (o serioso) ma ogni tanto –semel in anno …, avrebbe detto la Berta, la prof di ‘taliano, non troppo saepe, nèh– su una cosa che non so bene ma istruttiva –forse. O forse no.

Insomma c’è questa storia delle scimmie. Che ti ritrovi da tutte le parti, ce ne sono di davvero divertenti, in barili.

peterTutto è esploso con il Lisp, Common Lisp. E Peter Seibel. In effetti mi sento in colpa: sto copiando dal suo Practical Common Lisp come se –ecco troppo! Vale dire che è fatto molto bene, completo. Sto capendo tutto, anche se sono niubbo assay e anche non troppo smart. Fatto sta ed è che il sito di Peter si chiama Gigamonkeys, e lì trovate anche il perché.

Poi ci sono altri gigamonkeys on-teh-toobz, ecco qualche logos, uncorrelati:

logos
Ma forse no, c’è altro, davvero. In questi casi Urban Dictionary è una miniera. Adesso non voglio fare quello che linka tutto, potete –se avete la mia stessa fisima– cercare voi, io riassumo.

udEcco una parola di successo, di quelle che uno proprio non può farne a meno.

clouseau

Visto nel Web – 175

Puntuale come un Rolex a testa alta ecco cosa ho visto nel Web.

MIT Launches Three-pronged Effort To Thwart Cyber Attacks
::: Slashdot

Whitehouse.gov Is Now Secure By Default, Signaling a Trend in Government
::: Motherboard

L’Internet delle cose
::: ElectroYou

USEFUL ONE-LINE SCRIPTS FOR SED
::: sed

4chan’s Overlord Christopher Poole Reveals Why He Walked Away
::: Rolling Stone

maurizio

Early wearable computer: Qing Dynasty abacus ring
::: BaxterSally

Nancy Drew and the Case of the Slow Program
altamente istruttivo
::: Julia Evans

Continue statements with Labels in Go (golang)
::: Repeatable Systems

Ognuno ha il suo sticky bit che si merita!
Lubit cresce, bene :grin:
::: the secrets of ubuntu

Why is it hard to write a compiler for Perl 6?
::: steveklabnik

1329151-004-U430304105092110QB-U430702588488417YG-593x443@Corriere-Web-Nazionale

This App Lets You Piggyback Facebook’s Free Internet To Access Any Site
::: Slashdot

The GNU Manifesto Turns Thirty
::: The New Yorker

Addomesticare PowerPoint in 10 mosse
::: Sergio Gridelli

GXUI – A Go cross platform UI library
::: google

Smartphones set to boost large-scale health studies
::: Nature

tumblr_nlct2rkvcd1qz5v2lo1_500

Not Quite Dead: SCO Linux Suit Against IBM Stirs In Utah
::: Slashdot

Congratulations to @Wikimedia for adopting #OA policy for funded research – great example for other private funders
::: communia_eu

Survey: C++ is the most valuable software engineering skill
::: ITworld

Microsoft is open sourcing Visual Studio’s build tool, MSBuild
::: The Next Web

Dale
Lisp-flavoured C
::: tomhrr

21114_10205118677599977_3863802669928651831_n

Come Programmare Su Linux
::: Andrea M

Il Paper inviato dal Governo Renzi alla Commissione Europea. La posizione Italiana sul digital single MARKET
::: FULOG

Pensare in Python – Come pensare da Informatico, la nuova versione italiana
::: Distillato di Python

Primo studio per il simbolo
::: ulaulaman ::: Flavio_MfM

#Lubit6 #LubitProject
::: bit3lux

11075192_1049690921714835_9049442868188114297_n

Il @pewresearch sugli effetti di Internet nei paesi in via di sviluppo
::: fabiochiusi

Point to point wiring of LISP
::: walltechOSHW

È colpa tua @google :(
::: SteMicDM

Writing And Using C Code From Go
::: Karl Seguin

Clojure to JS compiler
::: clojure

162438494-09de8be5-f45b-4edc-9f33-e08dd2850ed4

Defending Privacy Doesn’t Pay: Canadian Court Lets Copyright Troll Off the Hook
::: Slashdot

Linux’s worst-case scenario: Windows 10 makes Secure Boot mandatory, locks out other operating systems
::: ExtremeTech

RMS, do you have an opinion about systemd?
::: johns_FSF

Virtual Keypunch
type and download your personal punch cards
::: mass:werk
11026318_961750537203359_7264993884924288622_n

Lisp – Macros: costrutti di controllo standard – 2

beagle-bros-statement-of-qualityContinuo da qui, copiando da qui.

Cicli

Oltre ai costrutti di controllo visti nel post precedente ci sono i cicli, l’altro costrutto. Tutti i costrutti per i cicli sono macro, costruiti su un paio di operatori speciali che forniscono una specie primitiva di goto. Sono tagbody e go, li vedremo in futuro. Il tipo di ciclo più generico è quello di do. Benché potente per i casi normali ci sono soluzioni più semplici, dolist e dotimes. Infine la macro loop fornisce un completo mini-linguaggio à la Algol-like. Alcuni amano loop, altri lo odiano dicendo che non è lispico, vedi te!

dolist e dotimes

dolist cicla attraverso gli elementi di una lista eseguendo il corpo con una variabile contenente i successivi elementi della lista. La forma base è:

(dolist (var list-form)
  body-form*)

Quando il ciclo parte list-form è valutata per produrre una lista. Quindi il corpo del ciclo è valutato una volta per ogni elemento della lista con la variabile var avente il valore dell’elemento. Per esempio: m0 Usato in questo modo ritorna nil. È possibile interromperlo con return. m1 dotimes è per i cicli con contatore, la forma base è:

(dotimes (var count-form)
  body-form*)

count-form deve ritornare un intero. Il ciclo mette in var i valori da 0 a uno meno di quel numero, come in Python: m2anche qui è possibile usare return. Siccome il corpo di dotimes e dolist può contenere ogni tipo di espressioni si possono fare cicli annidati. per esempio le tabelline: m3

do

dotimes e dolist non sono sufficientemente flessibili per poter essere usati in tutti i cicli. Per esempio quando voglio gestire passi con variabili in parallelo o usare un’espressione generica per il test per la fine del ciclo. In questi casi si passa al più generico ciclo do. Mentre per dotimes e dolist ho una sola variabile che controlla il ciclo con do posso avere il controllo completo di come cambiarle nel ciclo. È anche possibile di definire un test che determina quando terminare il ciclo e fornire una form da eseguire alla fine che genera il valore ritornato dal do. Il modello base è:

(do (variable-definition*)
    (end-test-form result-form*)
  statement*)

Le sei parentesi del modello sono le sole richieste per il do stesso. Devi fornire una coppia per racchiudere le dichiarazioni di variabile, una coppia per il test finale e una coppia per l’intera espressione. Le forms entro il do possono richiedere –ovviamente– loro parentesi ma lo schema dev’essere sempre quello. Qualche esempio: m4 in questo caso il risultato della form è omesso e tutto equivale a un dotimes. Altro esempio, Fibonacci, calcolato senza corpo: m5 Nota: mysteryousassay :shock: Infine un esempio con un do senza variabili collegate. Cicla mentre il tempo attuale è inferiore a quello dato in una variabile globale, scrivendo “Waiting” una vola al minuto. Notare che anche senza variabile la coppia vuota di parentesi è richiesta. m6

Il grande loop

Allora… per i casi semplici ci sono dolist e dotimes, poi quando le cose sono più complesse do. Serve altro? Ci sono diversi casi in cui cicli su strutture come liste, arrays, hash tables e packages. O accumuli valori ciclando, contando, sommando, mediando e così via. Quando fai parecchie di queste cose allo stesso tempo la macro loop è quella che semplifica le cose. Nota personale: io vengo dal Fortran :wink: La macro loop esiste in due varianti: semplice e estesa. la versione semplice è semplice davvero:

(loop
  body-form*)

le forms nel corpo sono valutate ogni volta nel ciclo finché non esci con return. Per esempio il do precedente può essere così riscritto: m7 Il loop esteso è una bestia completamente differente, dice il Peter. È contraddistinto dall’uso di loop keywords che implementano un linguaggio ad-hoc per esprimere espressioni di looping. Ovvio che non tutti i lispers amino il linguaggio dell’extended loop, almeno uno dei progettisti di Common Lisp lo odiava :wink: I detrattori dicono che non è abbastanza lispico, troppe poche parentesi. I fautori dicono che in certi casi è meglio avere una sintassi più verbosa ma che rende l’idea di cosa succede. Per esempio ecco un ciclo do che mette i numeri da 1 a 10 in una lista: m8 Un lispista stagionato (cit.) non avrebbe difficoltà a capire al volo, è solo un do con nreverse e push per costruire una lista. Ma non è così trasparente. La versione con loop sembra quasi linguaggio corrente: m9 Oppure la somma dei primi 10 quadrati: m10 O il conteggio delle vocali in una stringa: m11 Il conteggio dell’undicesimo numero di Fibonacci, quello stesso del precedente do: m12 I simboli across, and, below, collecting, counting, finally, for, from, summing, then e to sono alcune delle keywords della sintassi di loop. Mi sa che di loop dovremmo parlarne ancora, molto più avanti. Ma è da notare qui come le macro possono estendere il linguaggio rendendolo più espressivo. loop ha una sua grammatica ma il resto del codice in loop è codice Lisp regolare. E anche se loop è molto più complessa di when e unless è solo un’altra macro. OK, adesso siamo pronti –dice Peter (che rockz, ogni tanto è bene ripeterlo)– per vedere più in dettaglio come fare a definire le nostre. Prossimamente… forse :mrgreen:

Lisp – Macros: costrutti di controllo standard – 1

lIl titolo l’ha messo Peter, io sarei più sul semplice, mica sono così smart :wink:
E il titolo speciale è perché si parla di una cosa speciale del Lisp, che è tutto speciale fià di suo. Sì le macro come le ha il Lisp non le ha nessuno (non contano le imitazioni).

Spiegare cosa siano le macro non è la cosa più semplice, come gli OGM soffrono di pregiudizi ma dai, pronti? via!

È normale che con ogni linguaggio di programmazione oltre al core ci siano librerie, che implementano un po’ tutto quello che serve. Uno dei vantaggi di questo approccio è che è più facile da implementare e comprendere. E, il vantaggio reale, è estensibile, cosa che per il Common Lisp lo fanno le macro.
Abbiamo già visto brevemente che ogni macro definisce la sua propria sintassi, determinando come le s-expressions sono passate alle Lisp forms. Con le macro del core è possibile creare nuove sintassi –costrutti di controllo come when, dolist e loop e di definizione di forms come defun e defparameter– come parte della standard library invece di doverle inserire dentro il core.
Salto qui una tiratina contro la macrophobia derivata da C e dintorni :roll: Potete sempre –volendo– leggerla di là.

when e unless

Abbiamo già visto la forma condizionale base: if:

(if condition then-form [else-form])

condition è valutata e, se non-nil, la then-form è valutata e il suo valore ritornato; altrimenti l’else-form è valutata e il suo valore ritornato. Se condition è nil e non c’è else-form il risultato di if è nil.

(if (> 2 3) "Yup" "Nope") ==> "Nope"
(if (> 2 3) "Yup")        ==> NIL
(if (> 3 2) "Yup" "Nope") ==> "Yup"

Tuttavia if non è così bella come vorremmo perché then-form e else-form sono confinate entro un unica form. Questo comporta difficoltà quando voglio effettuare più azioni, per esempio, caso di trattamento dello spam devo scrivere (semplifico qui, Peter la fa più lunga), usando progn che esegue tutte le sue form tornando il valore dell’ultima:

(if (spam-p current-message)
    (progn
      (file-in-spam-folder current-message)
      (update-spam-database current-message)))

OK, si può fare ma siccome è una cosa abbastanza comune perché non fare qualcosa? Ecco, c’è when che consente di scrivere:

(when (spam-p current-message)
  (file-in-spam-folder current-message)
  (update-spam-database current-message))

Ma se non fosse definita nella standard library si potrebbe definire when, usando la backquote già vista anticamente:

(defmacro when (condition &rest body)
  `(if ,condition (progn ,@body)))

Nota: in realtà non si può ridefinire una macro della standard library, volendo proprio usare un altro nome, p.es. my-when.

Il complemento di when è unless, che fa l’opposto, valuta il suo corpo se condition è falsa.

(defmacro unless (condition &rest body)
  `(if (not ,condition) (progn ,@body)))

OK, sono macro davvero semplici, ma fanno il loro lavoro semplificando il codice. E ci fanno vedere come anche noi possiamo crearne di simili.

cond

Un altro caso dove if è insufficiente è quando ci sono più scelte: if a fai x, altrimenti if b fai y altrimenti fai z. Si può fare, così:

(if a
    (do-x)
    (if b
       (do-y)
       (do-z)))

ma –come dire– e probabilmente devi metterci di mezzo anche progn. Ovvio quindi che ci sia una macro per la condizione multipla cond:

(cond
  (test-1 form*)
      .
      .
      .
  (test-N form*))

Ogni elemento del corpo rappresenta una delle scelte della condizione e consiste in una lista contenente una form di condizione e zero o più forms che sono valutate se la scelta è scelta (ehmm…). Le condizioni sono valutate nell’ordine in cui compaiono finché una risulta vera. A questo punto le altre forms della scelta sono valutate e l’ultima è ritornata come valore della cond. Se non ci sono forms nella scelta è questa che viene ritornata. Per convenzione l’ultima scelta rappresentante l’altrimenti finale della catena di scelte è scritta come t. Ogni valore non-nil funzionerebbe ma così si vede chiaramente quando si legge il codice.
Quindi la precedente if annidata si può riscrivere così:

(cond (a (do-x))
      (b (do-y))
      (t (do-z)))

Scrivendo condizioni per if, when, unless e cond salta fuori l’utilità degli operatori logici and, or e not.

not (è una funzione) che inverte il valore della verità, tornando t se nil e nil se t.

and e or sono macro implementate come congiunzione e disgiunzione logiche di qualsiasi numero di subforms; sono definite come short-circuit, quindi ne sono valutate solo quanto serve. Quindi and torna nil non appena vede un nil; se nessuna vale nil torna il valore dell’ultima. or invece si ferma appena una vale non-nil e ne ritorna il valore. Se nessuna è vera torna nil. Esempi:

(not nil)             ==> t
(not (= 1 1))         ==> nil
(and (= 1 2) (= 3 3)) ==> nil
(or (= 1 2) (= 3 3))  ==> t

OK, adesso i cicli — no! pausa :mrgreen:

Iscriviti

Ricevi al tuo indirizzo email tutti i nuovi post del sito.

Unisciti agli altri 84 follower