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

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

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

Ответ 1

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

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

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

;; the following two functions just make equivalent of Python 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 определяет пару специальных синтаксических форм. Цитата (') указывает, что следующий токен является литералом. Квазиквасота или обратная сторона (`) указывает, что следующий токен является литералом с экранами. Выходы обозначаются запятой. Литерал '(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 или С# with. Или .NET LINQ синтаксис. В конце концов, это то, что привлекает людей к Lisp - максимальной гибкости.

Ответ 2

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

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

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

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

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

  (setf x whatever)

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

lisp также имеет списки; вы можете использовать функции "автомобиль" и "cdr" для получения первого элемента списка или остальной части списка соответственно.

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

  (setf (car somelist) whatever)

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

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

  (rplaca somelist whatever)

Ответ 3

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

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

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

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

Ответ 4

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.

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

Ответ 5

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

Ответ 6

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

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

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

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

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

define macro "using":
    using ($type $varname = $expression) $block
into:
    $type $varname;
    try {
       $varname = $expression;
       $block;
    } finally {
       $varname.Dispose();
    }

Ответ 7

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

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

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

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

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

Ответ 8

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

Lisp макросы отлично работают из-за синтаксиса Lisp.

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

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

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

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

Ответ 9

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

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

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

Ответ 10

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

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

Vatine

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

Мэтт Кертис

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

Мигель Пинг

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

Питер Сейбел, в разделе "Практическое общее Lisp"

Ответ 11

Короче говоря, макросы - это преобразования кода. Они позволяют вводить множество новых синтаксических конструкций. Рассмотрим, например, LINQ в С#. В lisp существуют аналогичные языковые расширения, которые реализуются макросами (например, встроенная конструкция цикла, итерация). Макросы значительно уменьшают дублирование кода. Макросы позволяют внедрять "маленькие языки" (например, где в С#/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))

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

Ответ 12

В то время как все выше описано, какие макросы есть и даже имеют классные примеры, я думаю, что ключевое различие между макросом и нормальной функцией состоит в том, что 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
    

Ответ 13

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

"Макрос - это обычный кусок кода lisp, который работает с другим куском предполагаемого кода 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

Ответ 14

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