Насколько полезны макросы Lisp?

22

Common Lisp позволяет вам писать макросы, которые выполняют любые преобразования исходного кода, которые вы хотите.

Схема предоставляет вам гигиеническую систему сопоставления с образцом, которая также позволяет вам выполнять преобразования. Насколько полезны макросы на практике? Пол Грэм сказал в « Побивке средних», что:

Исходный код редактора Viaweb, вероятно, содержал около 20-25% макросов.

Какие вещи люди на самом деле делают с макросами?

compman
источник
Я думаю, что это определенно относится к хорошим субъективным , я отредактировал ваш вопрос для форматирования. Это может быть дубликатом, но я не смог найти.
Тим Пост
1
Я полагаю, все, что не соответствует в функции.
2
Вы можете использовать макросы, чтобы превратить Lisp в любой другой язык с любым синтаксисом и любой семантикой: bit.ly/vqqvHU
SK-logic
На этом стоит посмотреть programmers.stackexchange.com/questions/81202/… , но это не дубликат.
Дэвид Торнли
1
stackoverflow.com/questions/2561221/…
Райнер Йосвиг

Ответы:

15

Посмотрите на эту публикацию Матиаса Феллайзена в списке обсуждения LL1 в 2002 году. Он предлагает три основных варианта использования макросов:

  1. Подъязыки данных : я могу писать простые выражения и создавать сложные вложенные списки / массивы / таблицы с кавычками, кавычками и т. Д., Аккуратно наряженные макросами.
  2. Связывающие конструкции : я могу представить новые связывающие конструкции с макросами. Это помогает мне избавиться от лямбд и расположить вещи ближе друг к другу, которые принадлежат друг другу.
  3. Изменение порядка оценки : я могу представить конструкции, которые задерживают / откладывают оценку выражений по мере необходимости. Подумайте о циклах, новых условных выражениях, задержке / форсировании и т. Д. [Примечание: в Haskell или на любом ленивом языке этот ненужный.]
Джон Клементс
источник
18

В основном я использую макросы для добавления новых языковых конструкций, экономящих время, которые в противном случае потребовали бы связки стандартного кода.

Например, недавно я обнаружил, что мне нужен императив, for-loopпохожий на C ++ / Java. Однако, будучи функциональным языком, Clojure не входил в комплект поставки. Так что я просто реализовал это как макрос:

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

И теперь я могу сделать:

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

И вот, у вас это есть - новая универсальная языковая конструкция во время компиляции из шести строк кода.

mikera
источник
13

Какие вещи люди на самом деле делают с макросами?

Написание языковых расширений или DSL.

Чтобы почувствовать это на Lisp-подобных языках, изучите Racket , который имеет несколько языковых вариантов: Typed Racket, R6RS и Datalog.

Смотрите также язык Boo, который дает вам доступ к конвейеру компилятора для конкретной цели создания доменных языков с помощью макросов.

Роберт Харви
источник
4

Вот некоторые примеры:

Схема:

  • defineдля определения функций. В основном это делает более короткий способ определения функции.
  • let для создания лексически ограниченных переменных.

Clojure:

  • defnсогласно его документам:

    То же, что (имя def (fn [params *] exprs *)) или (имя def (fn ([params *] exprs *) +)) с любой строкой документа или атрибутами, добавленными в метаданные var

  • for: список понятий
  • defmacro: ироничный?
  • defmethod, defmulti: работа с мульти-методами
  • ns

Многие из этих макросов значительно упрощают написание кода на более абстрактном уровне. Я думаю, что макросы во многом похожи на синтаксис не-лиспов.

Библиотека черчения Incanter предоставляет макросы для некоторых сложных манипуляций с данными.


источник
4

Макросы полезны для встраивания некоторых шаблонов.

Например, Common Lisp не определяет whileцикл, но имеет, do который может быть использован для его определения.

Вот пример из On Lisp .

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

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

Это выведет «12345678910», и если вы попытаетесь увидеть, что происходит с macroexpand-1:

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

Это вернет:

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

Это простой макрос, но, как уже было сказано, они обычно используются для определения новых языков или DSL, но из этого простого примера вы уже можете попытаться представить, что вы можете с ними сделать.

loopМакрос является хорошим примером того , что макросы могут сделать.

(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 имеет другой вид макросов, называемый макросом reader, который можно использовать для изменения интерпретации кода читателем, т.е. вы можете использовать их для использования # {и #} с разделителями, такими как # (и #).

Daimrod
источник
3

Вот что я использую для отладки (в 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

Мне приходилось иметь дело с хеш-таблицей, созданной вручную, в C ++, где getметод принял в качестве аргумента неконстантную ссылку на строку, то есть я не могу вызвать ее с помощью литерала. Чтобы было легче разобраться, я написал что-то вроде следующего:

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

Хотя что-то вроде этой проблемы вряд ли возникнет в lisp, мне особенно приятно, что у вас могут быть макросы, которые не оценивают свои аргументы дважды, например, путем введения реального let-связывания. (Допустим, здесь я мог бы обойти это).

Я также прибегаю к ужасно уродливому способу обернуть вещи в do ... while (false)такое, что вы можете использовать их в тогдашней части if и по-прежнему работать как-то иначе, как и ожидалось. Вам это не нужно в lisp, который является функцией макросов, работающих на синтаксических деревьях, а не на строках (или последовательностях токенов, я думаю, в случае C и C ++), которые затем подвергаются синтаксическому анализу.

Существует несколько встроенных макросов потоков, которые можно использовать для реорганизации вашего кода таким образом, чтобы он читался более чётко («многопоточность», как в «совмещении кода», а не параллелизм). Например:

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

Он принимает первую форму (range 6)и делает его последним аргументом следующей формы, (filter even?)который, в свою очередь, становится последним аргументом следующей формы и т. Д., Так что приведенное выше переписывается в

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

Я думаю, что первое читается гораздо яснее: «возьмите эти данные, сделайте это с ними, затем сделайте это, затем сделайте другое, и мы закончили», но это субъективно; объективно верно то, что вы читаете операции в той последовательности, в которой они выполняются (игнорируя лень).

Существует также вариант, который вставляет предыдущую форму в качестве первого (а не последнего) аргумента. Один вариант использования является арифметическим:

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

Читается как «возьмите 17, вычтите 2 и разделите на 3».

Говоря об арифметике, вы можете написать макрос, который выполняет синтаксический анализ инфиксных обозначений, так что вы могли бы сказать, например, (infix (17 - 2) / 3)и это вылилось бы (/ (- 17 2) 3)в недостаток, заключающийся в том, что он менее читабелен и имеет преимущество в том, что он является допустимым выражением lisp. Это часть подъязыка DSL / данных.

Йонас Кёлкер
источник
1
Применение функций имеет для меня гораздо больше смысла, чем многопоточность, но это, конечно, привычка. Хороший ответ.
coredump