Что вы можете сделать с макросами Lisp, которые вы не можете сделать с первоклассными функциями? - PullRequest
28 голосов
/ 12 февраля 2011

Мне кажется, я понимаю макросы Lisp и их роль на этапе компиляции.

Но в Python вы можете передать функцию в другую функцию

def f(filename, g):
  try:                                
     fh = open(filename, "rb") 
     g(fh)
  finally:
     close(fh) 

Итак, мы получаем ленивую оценкуВот.Что я могу сделать с макросами, а не с функциями в качестве объектов первого класса?

Ответы [ 7 ]

28 голосов
/ 12 февраля 2011

Прежде всего, в Лиспе есть и первоклассные функции, так что вы также можете спросить: «Зачем мне нужны макросы в Лиспе, если у меня уже есть первоклассные функции». Ответ на это заключается в том, что первоклассные функции не позволяют вам играть с синтаксисом.

На косметическом уровне первоклассные функции позволяют писать f(filename, some_function) или f(filename, lambda fh: fh.whatever(x)), но не f(filename, fh, fh.whatever(x)). Хотя, возможно, это хорошо, потому что в последнем случае гораздо менее понятно, откуда вдруг взялся fh.

Что более важно, функции могут содержать только действительный код. Таким образом, вы не можете написать функцию более высокого порядка reverse_function, которая принимает функцию в качестве аргумента и выполняет ее «в обратном порядке», так что reverse_function(lambda: "hello world" print) будет выполнять print "hello world". С помощью макроса вы можете сделать это. Конечно, этот конкретный пример довольно глуп, но эта возможность чрезвычайно полезна при внедрении языков, специфичных для предметной области.

Например, вы не можете реализовать общую конструкцию lisp loop в python. Черт, вы даже не могли бы реализовать конструкцию for ... in в python, если бы она не была встроенной - по крайней мере, с этим синтаксисом. Конечно, вы могли бы реализовать что-то вроде for(collection, function), но это гораздо менее красиво.

12 голосов
/ 12 февраля 2011

Вот ответ Матиаса Феллайзена с 2002 года (через http://people.csail.mit.edu/gregs/ll1-discuss-archive-html/msg01539.html):

Я бы хотел предложить три дисциплинированных использования макросов:

  1. подъязыки данных: я могу писать простые выражения и создавать сложные вложенные списки / массивы / таблицы с цитатой, аккуратно одетые и т.д. с макросами.

  2. связывающие конструкции: я могу представить новые связывающие конструкции с макросами. Это помогает мне избавиться от лямбды и с размещением вещей ближе друг к другу которые принадлежат друг другу. Например, один из наших учебных пакетов содержит форму
    (веб-запрос ([фамилия (string-append "Hello" имя-имя "каков ваш последний название?"]) ... фамилия ... имя ...) с очевидным взаимодействием между подразумевается программа и веб-потребитель.
    [Примечание: в ML вы могли бы написать веб-запрос (fn last-name => ...) string_append (...), но ей-богу это боль и ненужное рисунок.]

  3. переупорядочение оценки: я могу представить конструкции, которые задержать / отложить оценку выражения по мере необходимости. Подумай о петлях, новые условия, задержка / сила и т. д.
    [Примечание: в Хаскеле это не нужно один.]

Я понимаю, что Лисперс использует макросы по другим причинам. Честно говоря, я считаю, что это отчасти связано с недостатки компилятора, и частично из-за к "семантическим" нарушениям в язык перевода.

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

- Матиас

Феллайзен - один из самых влиятельных исследователей в области макроэкономики. (Я не знаю, согласится ли он с этим сообщением.)

Дополнительные материалы: Пол Грэм в «Лиспе» (http://www.paulgraham.com/onlisp.html; Грэм определенно не согласен с Феллайзеном в том, что это единственные полезные применения макросов), а также статья Шрирама Кришнамурти «Автоматы через макросы» "(http://www.cs.brown.edu/~sk/Publications/Papers/Published/sk-automata-macros/).

8 голосов
/ 13 февраля 2011

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

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

Очень простые преобразования кода

Создание простых языковых конструкций также очень простопример.Рассмотрим пример открытия файла:

(with-open-file (stream file :direction :input)
  (do-something stream))

против

(call-with-stream (function do-something)
                  file
                  :direction :input)

Что дает мне макрос - это немного другой синтаксис и структура кода.

Встроенный язык: расширенные итерационные конструкции

Далее рассмотрим немного другой пример:

(loop for i from 10 below 20 collect (sqr i))

против

(collect-for 10 20 (function sqr))

Мы можем определить функцию COLLECT-FOR который делает то же самое для простого цикла и имеет переменные для начала, конца и функции шага.

Но LOOP предоставляет новый язык.Макрос LOOP является компилятором для этого языка.Этот компилятор может выполнять LOOP определенные оптимизации, а также может проверять синтаксис во время компиляции для этого нового языка.Еще более мощный макрос цикла - ITERATE.Эти мощные инструменты на уровне языка теперь могут быть написаны как библиотеки без какой-либо специальной поддержки компилятора.

Обход дерева кода в макросе и внесение изменений

Далее еще один простойпример:

(with-slots (age name) some-person
  (print name)
  (princ " "
  (princ age))

противчто-то похожее:

(flet ((age (person) (slot-value person 'age))
       (name (person) (slot-value person 'name)))
   (print (name))
   (princ " ")
   (princ (age)))

Макрос WITH-SLOTS вызывает полный обход вложенного дерева исходных кодов и заменяет имя переменной вызовом (SLOT-VALUE SOME-PERSON 'name):

(progn
  (print (slot-value some-person 'name))
  (princ " "
  (princ (slot-value some-person 'age)))

В этомВ этом случае макрос может переписать выбранные части кода.Он понимает структуру языка Lisp и знает, что имя и возраст являются переменными.Он также понимает, что в некоторых ситуациях name и age не могут быть переменными и не должны быть переписаны.Это приложение так называемого Code Walker , инструмента, который может обходить деревья кода и вносить изменения в дерево кода.

Макросы могут изменять среду времени компиляции

Еще один простой пример, содержимое небольшого файла:

(defmacro oneplus (x)
  (print (list 'expanding 'oneplus 'with x))
  `(1+ ,x))

(defun example (a b)
   (+ (oneplus a) (oneplus (* a b))))

В этом примере нас интересует не макрос ONEPLUS, а макрос DEFMACROсама.

Что в этом интересного?В Лиспе вы можете иметь файл с указанным выше содержимым и использовать файл-компилятор для компиляции этого файла.

;;; Compiling file /private/tmp/test.lisp ...
;;; Safety = 3, Speed = 1, Space = 1, Float = 1, Interruptible = 1
;;; Compilation speed = 1, Debug = 2, Fixnum safety = 3
;;; Source level debugging is on
;;; Source file recording is  on
;;; Cross referencing is on
; (TOP-LEVEL-FORM 0)
; ONEPLUS

(EXPANDING ONEPLUS SOURCE A) 
(EXPANDING ONEPLUS SOURCE (* A B)) 
; EXAMPLE
;; Processing Cross Reference Information

Итак, мы видим, что файл-компилятор расширяетсяиспользование макроса ONEPLUS.

Что особенного в этом?В файле есть определение макроса, и в следующей форме мы уже используем этот новый макрос ONEPLUS.Мы никогда не загружали определение макроса в Лисп.Каким-то образом компилятор знает и регистрирует определенный макрос ONEPLUS и затем может использовать его.

Таким образом, макрос DEFMACRO регистрирует вновь определенный макрос ONEPLUS в среде времени компиляции, так чтоКомпилятор знает об этом макросе - без загрузки кода.Затем макрос может быть выполнен во время компиляции во время раскрытия макроса.

С помощью функции мы не можем этого сделать.Компилятор создает код для вызовов функций, но не запускает их.Но макрос можно запустить во время компиляции и добавить «знание» компилятору.Эти знания действительны во время работы компилятора и частично забыты позже.DEFMACRO - это макрос, который выполняется во время компиляции, а затем информирует окружение во время компиляции о новом макросе.

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

Это означает, что макрос, здесь DEFMACRO, может изменять язык и его среду во время компиляции файла.В других языках компилятор может предоставлять специальные директивы компилятора, которые будут распознаваться во время компиляции.Существует множество примеров определения таких макросов, влияющих на компилятор: DEFUN, DEFCLASS, DEFMETHOD, ...

Макросы могут сделать код пользователя короче

Типичным примером является макрос DEFSTRUCT для определения записи -подобных структур данных.

(defstruct person name age salary)

Выше defstruct макрос создает код для

  • новый тип структуры person с тремя слотами
  • средства доступа к слотам для чтения и записи значений
  • предикат для проверки того, принадлежит ли какой-либо объект класса person
  • make-person функция для создания объектов структуры
  • печатное представление

Дополнительно может:

  • записать исходный код
  • записать источник исходного кода (файл, буфер редактора, REPL, ...)
  • перекрестная ссылка на исходный код

Исходный код для определения структуры является короткимлиния.Развернутый код намного длиннее.

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

8 голосов
/ 12 февраля 2011

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

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

См. Ответы на этот вопрос для получения более подробной информации: Коллекция великолепных приложений и программ с использованием макросов

4 голосов
/ 13 февраля 2011

Вместо высокоуровневого ответа, вот конкретное предложение: прочитайте Шрирам * Свинья перед Perl Я покажу, как разработать макрос, который выполняет несколько разных вещей - конкретный поток управления, привязку и язык данных. (Кроме того, вы увидите, как на самом деле делать такие вещи.)

2 голосов
/ 20 ноября 2013

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

В Python вы не можете взять некоторый код, передать его функции, которая генерирует новый код, для выполнения нового кода. Чтобы достичь чего-то похожего на макросы в Python, вы должны сгенерировать AST из своего кода Python, изменить AST и оценить измененный AST.

Это означает, что вы не можете написать оператор if на Python. Вы можете использовать только существующий, но вы не можете изменить его или написать свои собственные заявления. Но макросы Lisp позволяют вам писать свои собственные утверждения. Например, вы можете написать оператор fi, который ведет себя как if, но принимает в качестве первого аргумента часть else , а часть затем - в качестве второго.

Следующая статья описывает разницу между макросами и процедурами более подробно: FTP: //ftp.cs.utexas.edu/pub/garbage/cs345/schintro-v13/schintro_130.html

0 голосов
/ 30 июля 2014

В примере, отличном от lisp, например, elixir, оператор потока управления if фактически является макросом. if реализован как функция. Но чтобы иметь более понятный и запоминающийся синтаксис, он также был реализован как макрос.

if true do 1+2 end
if true, do: ( 1+2 )
if(true, do: 1+2)
if(true, [do: (1+2)])
if(true, [{:do, 1+2}])

Все вышеперечисленное эквивалентно. Но первая строка - это реализация макроса if, которая предположительно расширяется в функцию if ниже.

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

is_number(if true do 1+2 end)
is_number(if true, do: (1+2))

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

...