Quanto sono utili le macro Lisp?


22

Common Lisp ti permette di scrivere macro che fanno qualunque trasformazione di sorgente tu voglia.

Scheme offre un sistema igienico di abbinamento dei modelli che consente anche di eseguire trasformazioni. Quanto sono utili le macro in pratica? Paul Graham ha dichiarato in Beating the Averages che:

Il codice sorgente dell'editor Viaweb era probabilmente circa il 20-25% di macro.

Che tipo di cose le persone finiscono per fare con le macro?


Penso che questo rientri sicuramente in un buon soggettivo , ho modificato la tua domanda per la formattazione. Questo potrebbe essere un duplicato, ma non sono riuscito a trovarlo.
Tim Post

1
Tutto ciò che è ripetitivo che non sembra adattarsi a una funzione, immagino.

2
Puoi usare le macro per trasformare Lisp in qualsiasi altra lingua, con qualsiasi sintassi e semantica: bit.ly/vqqvHU
SK-logic

programmers.stackexchange.com/questions/81202/… vale la pena dare un'occhiata qui, ma non è un duplicato.
David Thornley,

Risposte:


15

Dai un'occhiata a questo post di Matthias Felleisen nella lista di discussione LL1 del 2002. Suggerisce tre usi principali per le macro:

  1. Lingue secondarie dei dati : posso scrivere espressioni dall'aspetto semplice e creare elenchi / matrici / tabelle nidificate complesse con virgolette, virgolette, ecc. Ordinatamente decorate con macro.
  2. Costrutti vincolanti : posso introdurre nuovi costrutti vincolanti con le macro. Questo mi aiuta a liberarmi delle lambda e a mettere le cose più vicine tra loro che appartengono insieme.
  3. Riordino della valutazione : posso introdurre costrutti che ritardano / pospongono la valutazione delle espressioni secondo necessità. Pensa a loop, nuovi condizionali, ritardo / forza, ecc. [Nota: in Haskell o in qualsiasi linguaggio pigro, questo non è necessario.]

18

Uso principalmente le macro per aggiungere costrutti di linguaggio nuovi che fanno risparmiare tempo, che altrimenti richiederebbero un sacco di codice boilerplate.

Ad esempio, di recente mi sono trovato a desiderare un imperativo for-loopsimile a C ++ / Java. Tuttavia, essendo un linguaggio funzionale, Clojure non ne ha fornito uno fuori dagli schemi. Quindi l'ho appena implementato come macro:

(defmacro for-loop [[sym init check change :as params] & steps]
  `(loop [~sym ~init value# nil]
     (if ~check
       (let [new-value# (do ~@steps)]
         (recur ~change new-value#))
       value#)))

E ora posso fare:

 (for-loop [i 0 , (< i 10) , (inc i)] 
   (println i))

E il gioco è fatto: un nuovo costrutto di linguaggio in fase di compilazione per scopi generici in sei righe di codice.


13

Che tipo di cose le persone finiscono per fare con le macro?

Scrivere estensioni di lingua o DSL.

Per avere un'idea di ciò in linguaggi simili a Lisp, studia Racket , che ha diverse varianti linguistiche: Typed Racket, R6RS e Datalog.

Vedi anche il linguaggio Boo, che ti dà accesso alla pipeline del compilatore allo scopo specifico di creare linguaggi specifici del dominio attraverso le macro.


4

Ecco alcuni esempi:

Schema:

  • defineper le definizioni delle funzioni. Fondamentalmente rende un modo più breve per definire una funzione.
  • let per la creazione di variabili con ambito lessicale.

Clojure:

  • defn, secondo i suoi documenti:

    Come per (def name (fn [params *] exprs *)) o (def name (fn ([params *] exprs *) +)) con qualsiasi stringa di documenti o attr aggiunti ai metadati var

  • for: comprensione delle liste
  • defmacro: ironico?
  • defmethod, defmulti: lavorare con più metodi
  • ns

Molte di queste macro rendono molto più semplice scrivere codice a un livello più astratto. Penso che le macro siano simili, in molti modi, alla sintassi in non-Lisps.

La libreria di stampa Incanter fornisce macro per alcune manipolazioni complesse di dati.


4

Le macro sono utili per incorporare alcuni motivi.

Ad esempio, Common Lisp non definisce il whileloop ma ha do quali possono essere usati per definirlo.

Ecco un esempio da On Lisp .

(defmacro while (test &body body)
  `(do ()
       ((not ,test))
     ,@body))

(let ((a 0))
  (while (< a 10)
    (princ (incf a))))

Questo stamperà "12345678910" e se provi a vedere cosa succede con macroexpand-1:

(macroexpand-1 '(while (< a 10) (princ (incf a))))

Questo restituirà:

(DO () ((NOT (< A 10))) (PRINC (INCF A)))

Questa è una macro semplice, ma come detto in precedenza, vengono solitamente utilizzati per definire nuove lingue o DSL, ma da questo semplice esempio puoi già provare a immaginare cosa puoi fare con loro.

La loopmacro è un buon esempio di cosa possono fare le macro.

(loop for i from 0 to 10
      if (and (= (mod i 2) 0) i)
        collect it)
=> (0 2 4 6 8 10)
(loop for i downfrom 10 to 0
      with a = 2
      collect (* a i))
=> (20 18 16 14 12 10 8 6 4 2 0)               

Common Lisp ha un altro tipo di macro chiamata macro del lettore che può essere usata per modificare il modo in cui il lettore interpreta il codice, cioè puoi usarli per usare # {e #} ha delimitatori come # (e #).


3

Eccone uno che uso per il debug (in Clojure):

user=> (defmacro print-var [varname] `(println ~(name varname) "=" ~varname))
#'user/print-var
=> (def x (reduce * [1 2 3 4 5]))
#'user/x
=> (print-var x)
x = 120
nil

Ho dovuto fare i conti con una tabella hash arrotolata a mano in C ++, in cui il getmetodo ha preso come riferimento un riferimento a una stringa non const, il che significa che non posso chiamarlo con un valore letterale. Per semplificare la gestione, ho scritto qualcosa di simile al seguente:

#define LET(name, value, body)  \
    do {                        \
        string name(value);     \
        body;                   \
        assert(name == value);  \
    } while (false)

Mentre è improbabile che qualcosa di simile a questo problema si presenti in lisp, trovo particolarmente bello che tu possa avere macro che non valutano i loro argomenti due volte, ad esempio introducendo un vero e proprio binding. (Ammesso, qui avrei potuto aggirarlo).

Ricorro anche al brutto trucco di avvolgere roba in modo do ... while (false)tale che tu possa usarla nell'allora parte di un if e avere ancora il lavoro dell'altra parte come previsto. Non è necessario questo in lisp, che è una funzione delle macro che operano sugli alberi di sintassi piuttosto che sulle stringhe (o sequenze di token, credo, nel caso di C e C ++) che poi subiscono l'analisi.

Esistono alcune macro di threading integrate che possono essere utilizzate per riorganizzare il codice in modo che legga in modo più pulito ("threading" come "seminare il codice insieme", non parallelismo). Per esempio:

(->> (range 6) (filter even?) (map inc) (reduce *))

Prende la prima forma (range 6)e ne fa l'ultimo argomento della forma successiva (filter even?), che a sua volta viene trasformata nell'ultima discussione della forma successiva e così via, in modo tale che quanto sopra venga riscritto in

(reduce * (map inc (filter even? (range 6))))

Penso che la prima sia molto più chiara: "prendi questi dati, fai questo, poi fai quello, poi fai l'altro e abbiamo finito", ma è soggettivo; qualcosa che è oggettivamente vero è che leggi le operazioni nella sequenza in cui sono eseguite (ignorando la pigrizia).

Esiste anche una variante che inserisce la forma precedente come primo (anziché ultimo) argomento. Un caso d'uso è l'aritmetica:

(-> 17 (- 2) (/ 3))

Legge "prendi 17, sottrai 2 e dividi per 3".

Parlando di aritmetica, puoi scrivere una macro che aggiorna l'analisi della notazione, in modo da poter dire ad esempio (infix (17 - 2) / 3)e sputerebbe fuori (/ (- 17 2) 3)che ha lo svantaggio di essere meno leggibile e il vantaggio di essere un'espressione lisp valida. Questa è la parte in sublanguage DSL / data.


1
L'applicazione delle funzioni ha molto più senso del threading, ma sicuramente è una questione di abitudine. Bella risposta.
coredump,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.