Jak przydatne są makra Lisp?

22

Common Lisp pozwala pisać makra, które wykonują dowolną transformację źródłową.

Schemat zapewnia higieniczny system dopasowywania wzorów, który pozwala również przeprowadzać transformacje. Jak przydatne są makra w praktyce? Paul Graham powiedział w Beating the Averages, że:

Kod źródłowy edytora Viaweb zawierał prawdopodobnie około 20-25% makr.

Jakie rzeczy ludzie faktycznie robią z makrami?

compman
źródło
Myślę, że to zdecydowanie pasuje do subiektywnych , zredagowałem twoje pytanie dotyczące formatowania. To może być duplikat, ale nie mogłem go znaleźć.
Tim Post
1
Wszystko, co powtarzalne, nie wydaje się pasować do funkcji, tak sądzę.
2
Możesz użyć makr, aby zmienić Lisp w dowolny inny język, z dowolną składnią i dowolną semantyką: bit.ly/vqqvHU
SK-logic
programmers.stackexchange.com/questions/81202/... warto przyjrzeć się tutaj, ale nie jest to duplikat.
David Thornley,

Odpowiedzi:

15

Spójrz na ten post Matthiasa Felleisena na liście dyskusji LL1 w 2002 roku. Sugeruje trzy główne zastosowania makr:

  1. Podjęzyki danych : Potrafię pisać proste wyrażenia i tworzyć złożone zagnieżdżone listy / tablice / tabele z cytatem, bez cudzysłowu itp. Starannie ubrane w makra.
  2. Konstrukcje wiążące : Mogę wprowadzić nowe konstrukcje wiążące za pomocą makr. To pomaga mi pozbyć się lambdas i umieścić rzeczy bliżej siebie, które do siebie pasują.
  3. Zmiana kolejności oceny : Potrafię wprowadzić konstrukty, które opóźniają / odraczają ocenę wyrażeń w razie potrzeby. Pomyśl o pętlach, nowych warunkach, opóźnieniu / sile itp. [Uwaga: W Haskell lub innym leniwym języku ten nie jest konieczny.]
John Clements
źródło
18

Najczęściej używam makr do dodawania nowych, oszczędzających czas konstrukcji języka, które w innym przypadku wymagałyby dużej ilości kodu z podstawowymi informacjami.

Na przykład niedawno odkryłem, że chcę imperatywu for-looppodobnego do C ++ / Java. Ponieważ jednak był językiem funkcjonalnym, Clojure nie wyszedł z niego po wyjęciu z pudełka. Właśnie zaimplementowałem to jako makro:

(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#)))

A teraz mogę zrobić:

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

I oto masz - nowa konstrukcja języka ogólnego do kompilacji w sześciu liniach kodu.

mikera
źródło
13

Jakie rzeczy ludzie faktycznie robią z makrami?

Pisanie rozszerzeń językowych lub DSL.

Aby to sprawdzić w językach podobnych do Lisp , zapoznaj się z Racket , który ma kilka wariantów językowych: Typed Racket, R6RS i Datalog.

Zobacz także język Boo, który daje dostęp do potoku kompilatora w konkretnym celu tworzenia języków specyficznych dla domeny za pomocą makr.

Robert Harvey
źródło
4

Oto kilka przykładów:

Schemat:

  • definedla definicji funkcji. Zasadniczo jest to krótszy sposób definiowania funkcji.
  • let do tworzenia zmiennych o zasięgu leksykalnym.

Clojure:

  • defn, zgodnie z jego dokumentami:

    Taki sam jak (def name (fn [params *] exprs *)) lub (def name (fn ([params *] exprs *) +)) z dowolnym łańcuchem doc lub atrybutami dodanymi do metadanych var

  • for: lista wyrażeń
  • defmacro: ironiczny?
  • defmethod, defmulti: praca z wieloma metodami
  • ns

Wiele z tych makr znacznie ułatwia pisanie kodu na bardziej abstrakcyjnym poziomie. Myślę, że makra są pod wieloma względami podobne do składni w wersjach innych niż Lisps.

Biblioteka drukowania Incanter zapewnia makra dla niektórych złożonych manipulacji danymi.


źródło
4

Makra są przydatne do osadzania niektórych wzorców.

Na przykład Common Lisp nie definiuje whilepętli, ale ma to, do czego można użyć do jej zdefiniowania.

Oto przykład z On Lisp .

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

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

Spowoduje to wydrukowanie „12345678910”, a jeśli spróbujesz zobaczyć, co się stanie z macroexpand-1:

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

Zwróci to:

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

To proste makro, ale jak powiedziano wcześniej, są one zwykle używane do definiowania nowych języków lub DSL, ale z tego prostego przykładu możesz już spróbować wyobrazić sobie, co możesz z nimi zrobić.

loopMakro jest dobrym przykładem tego, co może zrobić makra.

(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 ma inny rodzaj makr zwanych makrami czytników, których można używać do modyfikowania sposobu interpretacji kodu przez czytnika, tzn. Można ich używać do # # i #} ma ograniczniki takie jak # (i #).

Daimrod
źródło
3

Oto jeden, którego używam do debugowania (w 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

Musiałem poradzić sobie z ręcznie zwijaną tabelą skrótów w C ++, w której getmetoda przyjęła jako argument ciąg non-const, co oznacza, że ​​nie mogę tego nazwać dosłownie. Aby łatwiej sobie z tym poradzić, napisałem coś takiego:

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

Podczas gdy coś podobnego do tego problemu raczej nie pojawi się w lisp, uważam za szczególnie miłe, że możesz mieć makra, które nie oceniają ich argumentów dwa razy, na przykład poprzez wprowadzenie prawdziwego let-bind. (Przyznaję, że mogłem to obejść).

Uciekam się też do okropnie brzydkiego sposobu pakowania rzeczy w do ... while (false)taki sposób, że można go użyć w części „jeśli” i nadal wykonywać inne czynności zgodnie z oczekiwaniami. Nie potrzebujesz tego w lisp, który jest funkcją makr działających na drzewach składniowych, a nie na ciągach (lub sekwencjach tokenów, jak sądzę w przypadku C i C ++), które następnie są analizowane.

Istnieje kilka wbudowanych makr wątków, których można użyć w celu reorganizacji kodu, tak aby czytał on bardziej czytelnie („wątkowanie” jak w „zasiewie kodu razem”, a nie równoległości). Na przykład:

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

Przyjmuje pierwszą formę (range 6)i czyni ją ostatnim argumentem następnej formy, (filter even?)która z kolei jest ostatnim argumentem następnej formy i tak dalej, tak że powyższe zostaje przepisane na

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

Myślę, że pierwszy z nich brzmi znacznie jaśniej: „weź te dane, zrób to, następnie zrób to, a potem zrób drugie i gotowe”, ale to subiektywne; obiektywnie prawda polega na tym, że odczytujesz operacje w kolejności, w jakiej są wykonywane (ignorując lenistwo).

Istnieje również wariant, który wstawia poprzednią formę jako pierwszy (a nie ostatni) argument. Jeden przypadek użycia jest arytmetyczny:

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

Odczytuje jako „weź 17, odejmij 2 i podziel przez 3”.

Mówiąc o arytmetyki, możesz napisać makro, które analizuje zapis notacji poprawkowej, abyś mógł powiedzieć np. (infix (17 - 2) / 3)I wyplułoby to, (/ (- 17 2) 3)co ma tę wadę, że jest mniej czytelne i ma tę zaletę, że jest prawidłowym wyrażeniem lisp. To jest część językowa DSL / danych.

Jonas Kölker
źródło
1
Zastosowanie funkcji ma dla mnie znacznie większy sens niż wątkowanie, ale na pewno jest to kwestia przyzwyczajenia. Niezła odpowiedź.
coredump