Что делает макросы Lisp такими особенными?

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

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

15 ответов

Чтобы дать краткий ответ, макросы используются для определения языковых синтаксических расширений для Common Lisp или Domain-Specific Languages ​​(DSL). Эти языки встроены прямо в существующий код на Лиспе. Теперь DSL могут иметь синтаксис, похожий на Lisp (например, интерпретатор Prolog Питера Норвига для Common Lisp) или совершенно другой (например, Infix Notation Math для 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 для выполнения списка. На самом деле мы уже используем один макрос (макрос цикла).

Лисп определяет пару специальных синтаксических форм. Цитата (') указывает, что следующий токен является литералом. Квазицитат или обратный удар (`) указывает, что следующий токен является литералом с escape-символами. Побеги указаны оператором запятой. Буквальный '(1 2 3) является эквивалентом Python [1, 2, 3], Вы можете назначить его другой переменной или использовать на месте. Вы можете думать о `(1 2 ,x) как эквивалент Python [1, 2, x] где 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 или C# with синтаксис. Или.NET синтаксис LINQ. В конце концов, это то, что привлекает людей в Lisp - максимальная гибкость.

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

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

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

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

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

  (setf x whatever)

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

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

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

  (setf (car somelist) whatever)

установить машину из какого-то списка.

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

  (rplaca somelist whatever)

Общие макросы Lisp существенно расширяют "синтаксические примитивы" вашего кода.

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

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

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

Макросы 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%")

И это даже не входит в макросы Reader.

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

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

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

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

- ватин

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

- Мэтт Кертис

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

- Мигель Пинг

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

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

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

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

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

Если бы в 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();
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Хотя все вышесказанное объясняет, что такое макросы и даже имеют классные примеры, я думаю, что ключевое отличие между макросом и нормальной функцией заключается в том, что LISP сначала оценивает все параметры перед вызовом функции. С макросом все наоборот, LISP передает параметры, не оцененные, в макрос. Например, если вы передадите (+ 1 2) функции, функция получит значение 3. Если вы передадите это макросу, она получит список ( + 1 2). Это может быть использовано для создания невероятно полезных вещей.

  • Добавление новой структуры управления, например, цикл или деконструкция списка
  • Измерьте время, которое требуется для выполнения переданной функции. С функцией параметр будет оцениваться до того, как управление будет передано функции. С помощью макроса вы можете объединить ваш код между началом и остановкой вашего секундомера. Ниже приведен точно такой же код в макросе и функции, и результат очень отличается. Примечание. Это надуманный пример, и реализация была выбрана так, чтобы она была идентична, чтобы лучше подчеркнуть разницу.

    (defmacro working-timer (b) 
      (let (
            (start (get-universal-time))
            (result (eval b))) ;; not splicing here to keep stuff simple
        ((- (get-universal-time) start))))
    
    (defun my-broken-timer (b)
      (let (
            (start (get-universal-time))
            (result (eval b)))    ;; doesn't even need eval
        ((- (get-universal-time) start))))
    
    (working-timer (sleep 10)) => 10
    
    (broken-timer (sleep 10)) => 0
    

Короче говоря, макросы - это преобразования кода. Они позволяют вводить много новых синтаксических конструкций. Например, рассмотрим LINQ в C#. В lisp есть похожие языковые расширения, которые реализуются макросами (например, встроенная конструкция цикла, итерация). Макросы значительно уменьшают дублирование кода. Макросы позволяют встраивать "маленькие языки" (например, когда в C#/java для настройки используется xml, в lisp то же самое может быть достигнуто с помощью макросов). Макросы могут скрыть трудности использования библиотек.

Например, в LISP вы можете написать

(iter (for (id name) in-clsql-query "select id, name from users" on-database *users-database*)
      (format t "User with ID of ~A has name ~A.~%" id name))

и это скрывает все содержимое базы данных (транзакции, правильное закрытие соединения, выборку данных и т. д.), тогда как в C# это требует создания SqlConnections, SqlCommands, добавления SqlParameters к SqlCommands, зацикливания на SqlDataReaders, правильного их закрытия.

Однострочный ответ:

Минимальный синтаксис => Макросы над выражениями => Краткость => Абстракция => Мощь


Макросы Лиспа не делают ничего, кроме написания кода программно. То есть после расширения макросов вы получили не что иное, как Лисп-код без макросов. Так что, в принципе , ничего нового они не добились.

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

Как показано во многих примерах в этой ветке, а также в книге Пола Грэма On Lisp , макросы lisp могут быть инструментом, позволяющим сделать ваш код намного более кратким. Когда краткость достигает точки, она предлагает новые уровни абстракции, чтобы код был намного чище. Возвращаясь снова к первому пункту, в принципе ничего нового они не предлагают, но это все равно, что сказать, что поскольку бумага и карандаши (почти) образуют машину Тьюринга, то настоящий компьютер нам не нужен.

Если вы немного разбираетесь в математике, подумайте, почему функторы и естественные преобразования — полезные идеи. В принципе ничего нового они не предлагают. Однако, расширив их до математики более низкого уровня, вы увидите, что для записи комбинации нескольких простых идей (с точки зрения теории категорий) может потребоваться 10 страниц. Какой из них вы предпочитаете?

Я получил это из обычной кулинарной книги LISP, но я думаю, что она объяснила, почему макросы LISP хороши в хорошем смысле.

"Макрос - это обычный фрагмент кода на Лиспе, который работает с другим фрагментом предполагаемого кода на Лиспе, переводя его в исполняемый Лисп (версия ближе к нему). Это может показаться немного сложным, поэтому давайте приведем простой пример. Предположим, вы хотите версия setq, которая устанавливает две переменные в одно и то же значение. Так что если вы напишите

(setq2 x y (+ z 3))

когда z=8 и x, и y установлены на 11. (Я не могу думать о какой-либо пользе для этого, но это только пример.)

Должно быть очевидно, что мы не можем определить setq2 как функцию. Если x=50 а также y=-5эта функция получит значения 50, -5 и 11; он не знал бы, какие переменные должны быть установлены. Что мы действительно хотим сказать, так это когда вы (система Lisp) видите (setq2 v1 v2 e), относиться к нему как к эквиваленту (progn (setq v1 e) (setq v2 e)), На самом деле, это не совсем правильно, но пока подойдет. Макрос позволяет нам сделать именно это, указав программу для преобразования входного шаблона (setq2 v1 v2 e)"в выходной шаблон (progn ...)".

Если вы подумали, что это хорошо, вы можете прочитать здесь: http://cl-cookbook.sourceforge.net/macros.html

В Python у вас есть декораторы, у вас есть функция, которая принимает другую функцию в качестве входных данных. Вы можете делать все, что захотите: вызывать функцию, делать что-то еще, оборачивать вызов функции в выпуске получения ресурсов и т. Д., Но вы не можете заглянуть внутрь этой функции. Скажем, мы хотели сделать его более мощным, скажем, ваш декоратор получил код функции в виде списка, тогда вы могли не только выполнять функцию как есть, но теперь вы можете выполнять ее части, переупорядочивать строки функции и т. Д.

Другие вопросы по тегам