[Этот ответ является попыткой объяснить, почему макросы и функции, которые не оценивают свои аргументы, - это разные вещи.Я считаю, что это относится к макросам в Clojure, но я не специалист по Clojure.Это также слишком долго, извините.]
Я думаю, что вы путаетесь между тем, что Лисп называет макросами, и конструкцией, которой нет в современном Лиспе, но которая раньше называлась FEXPR.
Тамэто две интересные, разные вещи, которые вам могут понадобиться:
- функции, которые при вызове не сразу оценивают свои аргументы;
- синтаксические преобразователи , которыев Lisp с именем macros .
Я буду разбираться с ними по порядку.
Функции, которые не сразу оценивают свои аргументы
Вобычный Lisp, форма типа (f x y ...)
, где f
- функция, будет:
- установит, что
f
- это функция, а не какая-то особенная вещь; - получить функцию, соответствующую
f
, и оценить x
, y
и остальные аргументы в некотором порядке, заданном языком (который может быть «в неуказанном порядке»); - call
f
с результатами оценки аргументов.
Шаг (1) необходим изначально, потому что f
может быть особой вещью (например, скажем if
или quote
), и, возможно, определение функции извлекается в (1) также: все это, а также порядок, в котором происходят вещи в (2), - это то, что язык должен определить (или, в случае схемы, скажем, оставить явно неопределенным).
Этопорядок, и, в частности, порядок (2) и (3) известен как аппликативный порядок или готовность к оценке (ниже я назову это аппликативным порядком).
Но есть и другие возможности.Одна из них заключается в том, что аргументы не оцениваются: функция вызывается, и только когда значения аргументов необходимы , они оцениваются.Для этого есть два подхода:
Первый подход заключается в определении языка таким образом, чтобы все функции работали таким образом.Это называется ленивая оценка или нормальный порядок оценка (ниже я назову это нормальным порядком).В языке обычного порядка аргументы функции оцениваются по волшебству в той точке, в которой они необходимы.Если они никогда не нужны, они могут вообще не оцениваться.Итак, на таком языке (я придумываю синтаксис для определения функции здесь, чтобы не фиксировать CL или Clojure или что-либо еще):
(def foo (x y z)
(if x y z))
Будет оцениваться только один из y
или z
при вызове foo
.
На языке обычного порядка вам не нужно явно заботиться о том, когда что-то оценивается: язык гарантирует, что они оцениваются к тому времени, когда они необходимы.
Языки нормального порядка кажутся очевидными, но с ними, как мне кажется, довольно сложно работать.Есть две проблемы, одна очевидная, а другая меньше: побочные эффекты
- происходят в менее предсказуемом порядке, чем в языках прикладного порядка, и могут вообще не возникать, поэтому люди привыкли писать нас императивным стилем (которым большинство людей) трудно с ними справиться;
- даже код без побочных эффектов может вести себя иначе, чем в языке аппликативного порядка.
Сторона-эффект проблемы может рассматриваться как не проблема: мы все знаем, что код с побочными эффектами плох, верно, так кого это волнует?Но даже без побочных эффектов все по-другому.Например, вот определение комбинатора Y на языке нормального порядка (это своего рода очень строгий, подмножество нормального порядка Схемы):
(define Y
((λ (y)
(λ (f)
(f ((y y) f))))
(λ (y)
(λ (f)
(f ((y y) f))))))
Если вы попытаетесь использовать эту версию Y вАппликативный язык порядка - как и обычная Схема - будет работать вечно.Вот аппликативная версия заказа Y:
(define Y
((λ (y)
(λ (f)
(f (λ (x)
(((y y) f) x)))))
(λ (y)
(λ (f)
(f (λ (x)
(((y y) f) x)))))))
Вы можете видеть, что это отчасти то же самое, но там есть дополнительные λs, которые по существу «ленизируют» оценку, чтобы остановить ее зацикливание.
Второй подход к нормальной оценке порядка - это иметь язык, которыйв основном аппликативный порядок, но в котором есть некоторый специальный механизм для определения функций, которые не оценивают свои аргументы.В этом случае часто требуется какой-то особый механизм для выражения в теле функции «теперь я хочу значение этого аргумента».Исторически такие вещи назывались FEXPRs , и они существовали в некоторых очень старых реализациях Lisp: в Lisp 1.5 они были, и я думаю, что и в MACLISP, и в InterLisp они были.
В аппликативномЧтобы упорядочить язык с помощью FEXPR, вам нужно как-то сказать «теперь я хочу оценить эту вещь», и я думаю, что это проблема, с которой сталкиваются: в какой момент вещь решает оценить аргументы?Что ж, в действительно старом Лиспе, который имеет чисто динамическую область видимости, есть отвратительный хак для этого: при определении FEXPR вы можете просто передать источник аргумента и затем, когда вы захотите его значение, выпросто позвоните EVAL
на это.Это просто ужасная реализация, потому что это означает, что FEXPR никогда не могут быть скомпилированы должным образом, и вам нужно использовать динамическую область видимости, чтобы переменные никогда не скомпилировались.Но вот как это сделали некоторые (все?) Ранние реализации.
Но эта реализация FEXPR допускает удивительный взлом: если у вас есть FEXPR, которому был передан источник его аргументов, и вы знаете, что этоВот как работают FEXPR, тогда он может манипулировать этим источником перед вызовом EVAL
для него: он может вызывать EVAL
для чего-то, полученного из источника.И, фактически, «источник», который он получает, даже не обязательно должен быть строго легальным Лиспом: это может быть что-то, что FEXPR знает, как манипулировать, чтобы сделать что-то, что есть.Это означает, что вы можете внезапно расширить синтаксис языка в общих чертах.Но стоимость возможности сделать это состоит в том, что вы не можете скомпилировать ничего из этого: синтаксис, который вы создаете, должен интерпретироваться во время выполнения, и преобразование происходит каждый раз, когда вызывается FEXPR.
Синтаксические преобразователи: macros
Таким образом, вместо того, чтобы использовать FEXPR, вы можете сделать что-то еще: вы можете изменить способ работы оценки так, чтобы, прежде чем что-либо еще происходило, был этап, на котором код проходил и, возможно,преобразован в некоторый другой код (возможно, более простой код).И это должно произойти только один раз: после того, как код был преобразован, полученная вещь может быть где-то спрятана, и преобразование больше не нужно повторять.Таким образом, процесс теперь выглядит следующим образом:
- код считывается и структура строится из него;
- эта первоначальная структура, возможно, преобразуется в другую структуру;
- (результирующая структура может быть скомпилирована);
- результирующая структура или результат ее компиляции оценивается, вероятно, много раз.
Итак, теперь процесс оценки разделен на несколько«времена», которые не перекрываются (или не перекрываются для конкретного определения):
- время чтения - это время, когда создается начальная структура;
- время макрорасширения - это время, когда оно преобразуется;
- время компиляции (что может не произойти) - это время, когда получаемый объект компилируется;
- время оценки - это время, когда оно оценивается.
Что ж, компиляторы для всех языков, вероятно, делают что-то вроде этого: перед тем, как действительно превратить ваш исходный код в то, что, как понимает машина, они будут делатьальl виды преобразования источника в источник.Но эти вещи находятся в кишечнике компилятора и работают с некоторым представлением источника, которое уникально для этого компилятора и не определяется языком.
Lisp открывает этот процесс для пользователей.Язык имеет две особенности, которые делают это возможным:
- структура, которая создается из исходного кода после того, как он был прочитан, определяется языком, и язык имеет богатый набор инструментов для манипулирования этой структурой.;
- созданная структура является довольно «низкой приверженностью» или строгой - во многих случаях она не особенно предрасполагает вас к какой-либо интерпретации.
В качестве примера второго пункта,рассмотрим (in "my.file")
: это вызов функции с именем in
, верно?Ну, может быть: (with-open-file (in "my.file") ...)
почти наверняка не вызов функции, а привязка in
к файловому дескриптору.
Из-за этих двух особенностей языка (а на самом деле некоторых других я не пойдув) Лисп может сделать замечательную вещь: он может позволить пользователям языка писать эти функции преобразования синтаксиса - макросы - в переносимом Лиспе .
Единственное, что остается, эторешить, как эти макросы должны быть записаны в исходном коде.И ответ такой же, как и у функций: когда вы определяете какой-то макрос m
, вы используете его просто как (m ...)
(некоторые Лиспы поддерживают более общие вещи, такие как символьные макросы CL . Во время расширения макроса- после того, как программа прочитана, но до того, как она (скомпилирована и) запущена, - система обходит структуру программы в поисках вещей, которые имеют определения макросов: когда она их находит, она вызывает функцию, соответствующую макросу с источникомкод, указанный его аргументами, и макрос возвращает некоторый другой фрагмент исходного кода, который проходит по очереди, пока не останется макросов (и да, макросы могут расширяться до кода, включающего другие макросы, и даже до кода, включающего их сами).этот процесс завершен, тогда результирующий код можно (скомпилировать и) запустить.
Таким образом, хотя макросы выглядят как вызовы функций в коде, они не просто функции, которые не оценивают ихаргументы, как FEXPR были: вместо этого они функции, которые занимают немногоf Исходный код Lisp и возврат еще одного бита исходного кода Lisp: они синтаксические преобразователи или функции, которые работают с исходным кодом (синтаксис) и возвращают другой исходный код.Макросы выполняются во время макроразвлечения, которое происходит раньше времени оценки (см. Выше).
Таким образом, на самом деле макросы являются функциями, написанными на Лиспе, и функции, которые они вызывают, оценивают свои аргументы совершенно условно: все совершенно обыденно.Но аргументами для макросов являются программы (или синтаксис программ, представленных в виде объектов Lisp), а их результаты (синтаксис) других программ.Макросы - это функции на мета-уровне, если хотите.Таким образом, макрос - это функция, которая вычисляет (части) программ: эти программы могут позже сами быть запущены (возможно, намного позже, возможно, никогда), после чего к ним будут применены правила оценки.Но в данный момент макрос называется тем, с чем он имеет дело, это просто синтаксисом программ, а не оценкой частей этого синтаксиса.
Итак, я думаю, что ваша ментальная модель такова, что макросы - это что-то вроде FEXPR, и в этом случае«Как оценивается аргумент» - это очевидная вещь.Но это не так: это функции, которые вычисляют программы, и они работают правильно до запуска программы, которую они вычисляют.
Извините, этот ответ был таким длинным и бессвязным.
Что случилось с FEXPR?
FEXPR всегда были довольно проблематичными.Например, что должен сделать (apply f ...)
?Поскольку f
может быть FEXPR, но обычно это невозможно узнать до времени выполнения, довольно сложно понять, что нужно делать.
Так что я думаю, что произошли две вещи:
- в тех случаях, когда люди действительно хотели языки нормального порядка, они реализовывали их, и для этих языков правила оценки касались проблем, с которыми пытались разобраться FEXPR;
- в языках аппликативного порядка, тогда, если вы не хотите оценивать какой-либо аргумент, вы теперь делаете это, явно говоря, что с помощью таких конструкций, как
delay
, создаете 'обещание' и force
, чтобы форсировать оценку обещания - потому чтоулучшенная семантика языков позволила полностью реализовать обещания на языке (CL не имеет обещаний, но их реализация по сути тривиальна).
Правильно ли я описал историю?
Я не знаю: я думаю, что это может быть, но это также может быть рациональной реконструкцией.Я, конечно, в очень старых программах в очень старых Лиспах видел FEXPR, которые используются так, как я описываю.Я думаю, что у бумаги Кента Питмана Специальные формы в Лиспе может быть некоторая история: я читал ее в прошлом, но забыл об этом до сих пор.