Оценка аргументов в функции, вызываемой макросом - PullRequest
0 голосов
/ 28 января 2019

Макросы не оценивают свои аргументы до тех пор, пока это не будет явно сказано, однако функции делают.В следующем коде:

(defmacro foo [xs]
  (println xs (type xs)) ;; unquoted list
  (blah xs))

(defn blah [xs] ;; xs is unquoted list, yet not evaluated
  (println xs)
  xs)

(foo (+ 1 2 3))

Кажется, что blah не оценивает xs, поскольку у нас все еще есть весь список: (+ 1 2 3) связан с xs в теле бла.

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

Я думал в основном: «Хорошо, в этом теле макроса у меня есть xs в качестве неоцененного списка, но если я вызываю функцию с xs из макроса, он должен оценить этот список».

Очевидно, что у меня есть смущающее фундаментальное недопонимание того, как все работает.Чего мне не хватает в моей интерпретации?Как на самом деле происходит оценка?

РЕДАКТИРОВАТЬ


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

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

Так что в приведенных выше примерах высказывание xs без кавычек несколько вводит в заблуждение.Например, этот макрос:

(defmacro bluh [xs]
  `(+ 1 2 ~xs))

В основном такой же, как приведенный ниже макрос (за исключением пространства имен на символах).Разрешение xs в вызове list возвращает неоцененный (цитируемый?) Список.

(defmacro bleh [xs]
  (list '+ '1 '2 xs)) ;; xs resolves to a quoted list (or effectively quoted)

Вызов bleh (или bluh) - это то же самое, что сказать:

(list '+ '1 '2 '(+ 1 2 3)) 
;; => (+ 1 2 (+ 1 2 3))

Если xs не преобразуется в цитируемый список, тогда мыв итоге:

(list '+ '1 '2 (+ 1 2 3)) 
;; => (+ 1 2 6)

Короче говоря, аргументы макросов цитируются .

Я думаю, что часть моей путаницы возникла из-за размышлений о синтаксисе в кавычкахв качестве шаблонов с заполненными слотами, например, (+ 1 2 ~xs), я мысленно расширил бы до (+ 1 2 (+ 1 2 3)), и, увидев, что (+ 1 2 3) не был указан в этом расширении, я обнаружил, что путают вызовы функций, используя xs (в первом примере выше blah) не будет сразу же переведено в 6.

Метафора шаблона полезна, но если я вместо этого смотрю на нее как на ярлык для (list '+ '1 '2 xs), становится очевидным, что xs должен быть списком в кавычках, иначе расширение будет включать 6, а не весь список.

Я не уверен, почему я нашел это настолько запутанным ... я правильно понял или я просто пошел по неверному пути полностью?

Ответы [ 2 ]

0 голосов
/ 30 января 2019

[Этот ответ является попыткой объяснить, почему макросы и функции, которые не оценивают свои аргументы, - это разные вещи.Я считаю, что это относится к макросам в Clojure, но я не специалист по Clojure.Это также слишком долго, извините.]

Я думаю, что вы путаетесь между тем, что Лисп называет макросами, и конструкцией, которой нет в современном Лиспе, но которая раньше называлась FEXPR.

Тамэто две интересные, разные вещи, которые вам могут понадобиться:

  • функции, которые при вызове не сразу оценивают свои аргументы;
  • синтаксические преобразователи , которыев Lisp с именем macros .

Я буду разбираться с ними по порядку.

Функции, которые не сразу оценивают свои аргументы

Вобычный Lisp, форма типа (f x y ...), где f - функция, будет:

  1. установит, что f - это функция, а не какая-то особенная вещь;
  2. получить функцию, соответствующую f, и оценить x, y и остальные аргументы в некотором порядке, заданном языком (который может быть «в неуказанном порядке»);
  3. callf с результатами оценки аргументов.

Шаг (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, вы можете сделать что-то еще: вы можете изменить способ работы оценки так, чтобы, прежде чем что-либо еще происходило, был этап, на котором код проходил и, возможно,преобразован в некоторый другой код (возможно, более простой код).И это должно произойти только один раз: после того, как код был преобразован, полученная вещь может быть где-то спрятана, и преобразование больше не нужно повторять.Таким образом, процесс теперь выглядит следующим образом:

  1. код считывается и структура строится из него;
  2. эта первоначальная структура, возможно, преобразуется в другую структуру;
  3. (результирующая структура может быть скомпилирована);
  4. результирующая структура или результат ее компиляции оценивается, вероятно, много раз.

Итак, теперь процесс оценки разделен на несколько«времена», которые не перекрываются (или не перекрываются для конкретного определения):

  1. время чтения - это время, когда создается начальная структура;
  2. время макрорасширения - это время, когда оно преобразуется;
  3. время компиляции (что может не произойти) - это время, когда получаемый объект компилируется;
  4. время оценки - это время, когда оно оценивается.

Что ж, компиляторы для всех языков, вероятно, делают что-то вроде этого: перед тем, как действительно превратить ваш исходный код в то, что, как понимает машина, они будут делатьаль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, которые используются так, как я описываю.Я думаю, что у бумаги Кента Питмана Специальные формы в Лиспе может быть некоторая история: я читал ее в прошлом, но забыл об этом до сих пор.

0 голосов
/ 28 января 2019

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

В вашем макросе foo вы определяете функцию макроса для возврата того, что blah сделал с вашим кодом 1012 *.Поскольку blah является (почти) функцией identity, она просто возвращает то, что было введено.

В вашем случае происходит следующее:

  • string "(foo (+ 1 2 3))" is read , создающий вложенный список с двумя символами и тремя целыми числами: (foo (+ 1 2 3)).
  • Символ foo разрешается в макрос foo.
  • Функция макроса foo вызывается с аргументом xs, связанным со списком (+ 1 2 3).
  • Функция макроса (печатает и затем) вызывает функцию blahсо списком.
  • blah (печатает и затем) возвращает этот список.
  • Функция макроса возвращает список.
  • Таким образом, макрос «расширяется» до (+ 1 2 3).
  • Символ + разрешается в функцию сложения.
  • Функция сложения вызывается с тремя аргументами.
  • Функция сложения возвращает их сумму.

Если вы хотите, чтобы макрос foo до расширил до вызова blah, вам нужно вернуть sтакая форма.Clojure предоставляет удобный синтаксис шаблонов с использованием обратной кавычки, так что вам не нужно использовать list и т. Д. Для построения кода:

(defmacro foo [xs]
  `(blah ~xs))

, например:

(defmacro foo [xs]
  (list 'blah xs))
...