Что делает макросы Lisp такими особенными? - PullRequest
280 голосов
/ 06 ноября 2008

Чтение Эссе Пола Грэма о языках программирования можно подумать, что Макросы Lisp - единственный путь. Как занятый разработчик, работающий на других платформах, я не имел права использовать макросы Lisp. Как человек, который хочет понять гул, пожалуйста, объясните, что делает эту функцию такой мощной.

Пожалуйста, также свяжите это с тем, что я бы понял из миров разработки на Python, Java, C # или C.

Ответы [ 14 ]

281 голосов
/ 07 января 2011

Чтобы дать краткий ответ, макросы используются для определения языковых синтаксических расширений для Common Lisp или Domain-Specific Languages ​​(DSL). Эти языки встроены прямо в существующий код на Лиспе. Теперь DSL могут иметь синтаксис, похожий на Lisp (например, Prolog Interpreter для Common Lisp Питера Норвига) или совершенно другой (например, Математическая система обозначений Infix для Clojure).

Вот более конкретный пример:
Python имеет встроенные в язык списки. Это дает простой синтаксис для общего случая. Линия

divisibleByTwo = [x for x in range(10) if x % 2 == 0]

возвращает список, содержащий все четные числа от 0 до 9. В течение дней Python 1,5 такого синтаксиса не было; вы бы использовали что-то вроде этого:

divisibleByTwo = []
for x in range( 10 ):
   if x % 2 == 0:
      divisibleByTwo.append( x )

Они оба функционально эквивалентны. Давайте вызовем нашу приостановку неверия и притворимся, что в Лиспе есть очень ограниченный макрос цикла, который просто выполняет итерации, и нет простого способа сделать эквивалентную работу со списком.

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

;; the following two functions just make equivalent of Python's range function
;; you can safely ignore them unless you are running this code
(defun range-helper (x)
  (if (= x 0)
      (list x)
      (cons x (range-helper (- x 1)))))

(defun range (x)
  (reverse (range-helper (- x 1))))

;; equivalent to the python example:
;; define a variable
(defvar divisibleByTwo nil)

;; loop from 0 upto and including 9
(loop for x in (range 10)
   ;; test for divisibility by two
   if (= (mod x 2) 0) 
   ;; append to the list
   do (setq divisibleByTwo (append divisibleByTwo (list x))))

Прежде чем идти дальше, я должен лучше объяснить, что такое макрос. Это преобразование, выполненное для кода кодом . То есть фрагмент кода, читаемый интерпретатором (или компилятором), который принимает код в качестве аргумента, манипулирует и возвращает результат, который затем запускается на месте.

Конечно, это много печатать, а программисты ленивы. Таким образом, мы могли бы определить DSL для выполнения списка. На самом деле мы уже используем один макрос (макрос цикла).

Lisp определяет несколько специальных синтаксических форм. Цитата (') указывает, что следующий токен является литералом. Квазицитат или обратный кавычка (`) указывает, что следующий токен является литералом с escape-символами. Побеги указаны оператором запятой. Литерал '(1 2 3) является эквивалентом [1, 2, 3] в Python. Вы можете назначить его другой переменной или использовать на месте. Вы можете думать о `(1 2 ,x) как о эквиваленте [1, 2, x] в Python, где x является переменной, определенной ранее. Эта запись списка является частью магии, которая входит в макросы. Вторая часть - это читатель Lisp, который разумно заменяет макросы на код, но это лучше всего показано ниже:

Таким образом, мы можем определить макрос с именем lcomp (сокращение от понимания списка). Его синтаксис будет точно таким же, как у питона, который мы использовали в примере [x for x in range(10) if x % 2 == 0] - (lcomp x for x in (range 10) if (= (% x 2) 0))

(defmacro lcomp (expression for var in list conditional conditional-test)
  ;; create a unique variable name for the result
  (let ((result (gensym)))
    ;; the arguments are really code so we can substitute them 
    ;; store nil in the unique variable name generated above
    `(let ((,result nil))
       ;; var is a variable name
       ;; list is the list literal we are suppose to iterate over
       (loop for ,var in ,list
            ;; conditional is if or unless
            ;; conditional-test is (= (mod x 2) 0) in our examples
            ,conditional ,conditional-test
            ;; and this is the action from the earlier lisp example
            ;; result = result + [x] in python
            do (setq ,result (append ,result (list ,expression))))
           ;; return the result 
       ,result)))

Теперь мы можем выполнить в командной строке:

CL-USER> (lcomp x for x in (range 10) if (= (mod x 2) 0))
(0 2 4 6 8)

Довольно аккуратно, а? Теперь это не останавливается там. У вас есть механизм или кисть, если хотите. Вы можете иметь любой синтаксис, какой только захотите. Как Python или синтаксис with в C #. Или .NET синтаксис LINQ. В конце концов, это то, что привлекает людей в Lisp - максимальная гибкость.

102 голосов
/ 06 ноября 2008

Здесь вы найдете исчерпывающую дискуссию о макросе lisp .

Интересное подмножество этой статьи:

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

Но Лисп другой. Макросы Lisp do имеют доступ к анализатору, и это действительно простой анализатор. Макрос на Лиспе представляет собой не строку, а предварительно подготовленный фрагмент исходного кода в виде списка, поскольку источником программы на Лиспе не является строка; это список. И программы на Лиспе действительно хорошо разбирают списки и объединяют их. Они делают это надежно, каждый день.

Вот расширенный пример. В Лиспе есть макрос, называемый "setf", который выполняет присваивание. Самая простая форма setf это

  (setf x whatever)

, который устанавливает значение символа "x" на значение выражения "что угодно".

Lisp также имеет списки; Вы можете использовать функции "car" и "cdr", чтобы получить первый элемент списка или остальную часть списка соответственно.

А что если вы хотите заменить первый элемент списка новым значением? Для этого есть стандартная функция, и невероятно, ее название даже хуже, чем «машина». Это "rplaca". Но вам не нужно помнить «rplaca», потому что вы можете написать

  (setf (car somelist) whatever)

, чтобы установить автомобиль Somelist.

Что здесь действительно происходит, так это то, что "setf" является макросом. Во время компиляции он проверяет свои аргументы и видит, что первый из них имеет форму (автомобиль НЕЧТО). Он говорит сам себе: «О, программист пытается что-то настроить. Для этого используется функция« rplaca »». И он спокойно переписывает код на месте:

  (rplaca somelist whatever)
53 голосов
/ 06 ноября 2008

Макросы Common Lisp существенно расширяют «синтаксические примитивы» вашего кода.

Например, в C конструкция switch / case работает только с целочисленными типами, и если вы хотите использовать ее для чисел с плавающей запятой или строк, у вас останутся вложенные операторы if и явные сравнения. Вы также не можете написать макрос на C, чтобы сделать эту работу за вас.

Но, так как макрос lisp - это (по сути) программа на lisp, которая принимает фрагменты кода в качестве входных данных и возвращает код для замены «вызова» макроса, вы можете расширить свой репертуар «примитивов» так далеко, как захотите, обычно заканчивается более читаемой программой.

Чтобы сделать то же самое в C, вам нужно написать собственный препроцессор, который съест ваш исходный (не совсем C) источник и выдаст то, что может понять компилятор C. Это не неправильный путь, но это не обязательно самый простой.

39 голосов
/ 06 ноября 2008

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

expr1 && expr2 && expr3 ...

То, что это говорит: Оцените expr1, и, если это правда, оцените expr2 и т. Д.

Теперь попробуйте превратить это && в функцию ... верно, вы не можете. Вызов что-то вроде:

and(expr1, expr2, expr3)

Оценит все три exprs, прежде чем дать ответ, независимо от того, был ли expr1 ложным!

С помощью макросов lisp вы можете кодировать что-то вроде:

(defmacro && (expr1 &rest exprs)
    `(if ,expr1                     ;` Warning: I have not tested
         (&& ,@exprs)               ;   this and might be wrong!
         nil))

теперь у вас есть &&, который вы можете вызывать точно так же, как функцию, и она не будет оценивать любые формы, которые вы ей передадите, если они все не верны.

Чтобы увидеть, как это полезно, контрастируйте:

(&& (very-cheap-operation)
    (very-expensive-operation)
    (operation-with-serious-side-effects))

и

and(very_cheap_operation(),
    very_expensive_operation(),
    operation_with_serious_side_effects());

Другие вещи, которые вы можете делать с макросами, - это создание новых ключевых слов и / или мини-языков (например, посмотрите макрос (loop ...)), интеграция других языков в lisp. напишите макрос, который позволит вам сказать что-то вроде:

(setvar *rows* (sql select count(*)
                      from some-table
                     where column1 = "Yes"
                       and column2 like "some%string%")

И это даже не входит в Считыватель макросов .

Надеюсь, это поможет.

28 голосов
/ 07 января 2011

Я не думаю, что когда-либо видел макросы Lisp, объясненные лучше, чем этот парень: http://www.defmacro.org/ramblings/lisp.html

10 голосов
/ 06 ноября 2008

Макрос lisp принимает фрагмент программы в качестве входных данных. Этот фрагмент программы представлен структурой данных, которой можно манипулировать и трансформировать любым удобным для вас способом. В конце макрос выводит другой фрагмент программы, и этот фрагмент - то, что выполняется во время выполнения.

C # не имеет средства макросов, однако эквивалентным было бы, если бы компилятор анализировал код в дереве CodeDOM и передавал его методу, который преобразовывал его в другой CodeDOM, который затем компилировался в IL.

Это может быть использовано для реализации синтаксиса "сахара", такого как for each -statement using -clause, linq select -expressions и т. Д., Как макросов, которые преобразуются в базовый код.

Если бы в Java были макросы, вы могли бы реализовать синтаксис Linq в Java без необходимости изменения базового языка Sun.

Вот псевдокод того, как может выглядеть макрос в стиле lisp в C # для реализации using:

define macro "using":
    using ($type $varname = $expression) $block
into:
    $type $varname;
    try {
       $varname = $expression;
       $block;
    } finally {
       $varname.Dispose();
    }
9 голосов
/ 06 ноября 2008

Подумайте, что вы можете сделать в C или C ++ с помощью макросов и шаблонов. Это очень полезные инструменты для управления повторяющимся кодом, но они весьма ограничены.

  • Ограниченный синтаксис макросов / шаблонов ограничивает их использование. Например, вы не можете написать шаблон, который расширяется до чего-то другого, кроме класса или функции. Макросы и шаблоны не могут легко поддерживать внутренние данные.
  • Сложный, очень нерегулярный синтаксис C и C ++ затрудняет написание очень общих макросов.

Макросы Lisp и Lisp решают эти проблемы.

  • Макросы Lisp написаны на Lisp. У вас есть все возможности Lisp для написания макроса.
  • Лисп имеет очень регулярный синтаксис.

Поговорите с кем-нибудь, кто освоил C ++, и спросите его, сколько времени они потратили на изучение всех шаблонов, необходимых им для выполнения метапрограммирования шаблонов. Или все сумасшедшие уловки в (превосходных) книгах, таких как Modern C ++ Design , которые все еще трудно отлаживать и (на практике) не переносимы между реальными компиляторами, даже если язык был стандартизирован в течение десятилетия , Все это исчезает, если язык, который вы используете для метапрограммирования, - это тот же язык, который вы используете для программирования!

8 голосов
/ 09 февраля 2014

Поскольку существующие ответы дают хорошие конкретные примеры, объясняющие, что макросы достигают и как, возможно, это помогло бы собрать воедино некоторые мысли о том, почему макрорежим является значительным преимуществом по сравнению с другими языками ; сначала из этих ответов, потом из другого:

... в C вам придется написать собственный препроцессор [который, вероятно, будет квалифицирован как достаточно сложная программа на C ] ...

& mdash; Vatine

Поговорите с кем-нибудь, кто освоил C ++, и спросите его, сколько времени они потратили на изучение всех шаблонов, которые им необходимы для выполнения метапрограммирования шаблонов [что еще не так эффективно].

- 1019 * Мэтт Кертис

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

- Мигель Пинг

DOLIST похож на foreach Perl или для Python for. Java добавила подобный вид конструкции цикла с «расширенным» циклом for в Java 1.5, как часть JSR-201. Обратите внимание на разницу в макросах. Программист на Лиспе, который замечает общий шаблон в своем коде, может написать макрос, чтобы получить абстракцию этого шаблона на уровне исходного кода. Программист Java, который замечает тот же шаблон, должен убедить Sun, что эта конкретная абстракция стоит добавить к языку. Затем Sun должна опубликовать JSR и создать отраслевую «экспертную группу», чтобы все прояснить. Этот процесс, согласно Sun, занимает в среднем 18 месяцев. После этого все авторы компиляторов должны обновить свои компиляторы для поддержки новой функции. И даже если любимый компилятор Java-программиста поддерживает новую версию Java, они, вероятно, «по-прежнему» не смогут использовать новую функцию, пока им не разрешат нарушить совместимость исходного кода со старыми версиями Java. Поэтому раздражение, которое программисты Common Lisp могут решить для себя за пять минут, мучает программистов на Java годами.

- Питер Сейбел, в «Практическом Обыкновенном Лиспе»

8 голосов
/ 06 ноября 2008

Я не уверен, что могу добавить некоторую проницательность к всем (превосходным) сообщениям, но ...

Макросы Lisp прекрасно работают из-за синтаксической природы Lisp.

Lisp - это чрезвычайно регулярный язык (подумайте обо всем: список ); макросы позволяют обрабатывать данные и код одинаково (для изменения выражений lisp не требуется разбора строк или других хаков). Вы комбинируете эти две функции, и у вас есть очень чистый способ изменить код.

Редактировать: То, что я пытался сказать, это то, что Lisp homoiconic , что означает, что структура данных для программы lisp написана в самом lisp.

Итак, вы в конечном итоге получаете способ создания собственного генератора кода поверх языка, используя сам язык со всей его мощью (например, в Java вам придется взломать свой путь с использованием байт-кода, хотя некоторые фреймворки, такие как AspectJ) позволяет сделать это, используя другой подход, это принципиально хак).

На практике, используя макросы, вы в конечном итоге создаете свой собственный мини-язык поверх lisp, без необходимости изучать дополнительные языки или инструменты, а также используя все возможности самого языка.

6 голосов
/ 06 ноября 2008

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

В объектах Python есть два метода __repr__ и __str__. __str__ - это просто читаемое человеком представление. __repr__ возвращает представление, которое является допустимым кодом Python, то есть что-то, что может быть введено в интерпретатор как действительный Python. Таким образом, вы можете создавать небольшие фрагменты Python, которые генерируют действительный код, который можно вставить в ваш исходный код.

В Лиспе весь этот процесс формализован макросистемой. Конечно, это позволяет вам создавать расширения синтаксиса и делать всякие причудливые вещи, но его фактическая полезность суммируется вышеизложенным. Конечно, помогает то, что макросистема Lisp позволяет вам манипулировать этими «фрагментами» с полной мощью всего языка.

...