Подтвердить что ты не робот

В какой степени макросы "функционируют обратным образом"?

Я пишу Lisp в Haskell (код в GitHub) как способ узнать больше об обоих языках.

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

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

eval env (List (function : args)) = do
    func <- eval env function
    case func of 
        (Macro {}) -> apply func args >>= eval env
        _          -> mapM (eval env) args >>= apply func

Он работает следующим образом:

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

Как будто макросы точно такие же, как функции, за исключением того, что порядок eval/apply переключается.

Это точное описание макросов? Не хватает ли чего-то важного, реализуя макросы таким образом? Если ответы "да" и "нет", то почему я раньше не видел, чтобы макросы объясняли это раньше?

4b9b3361

Ответ 1

Ответы "нет" и "да".

Похоже, вы начали с хорошей модели макросов, где уровень макросов и уровень времени выполнения находятся в разных мирах. Фактически, это один из основных моментов Racket макросистема. Вы можете прочитать небольшой текст об этом в руководстве Racket или просмотреть оригинальная статья, в которой описывается эта функция и почему это хорошая идея. Обратите внимание, что макетная система Racket является очень сложной, и она гигиенична, но разделение фаз является хорошей идеей, независимо от гигиены. Чтобы обобщить основное преимущество, он позволяет всегда расширять код надежным способом, поэтому вы получаете преимущества, такие как отдельная компиляция, и вы не зависите от порядка загрузки кода и таких проблем.

Затем вы переместились в одну среду, которая теряет это. В большинстве мира Lisp (например, в CL и в Elisp) это точно так, как делаются, и, очевидно, вы сталкиваетесь с проблемами, описанными выше. ( "Очевидное", поскольку разделение фаз было разработано таким образом, чтобы избежать этого, вы просто случайно получили свои открытия в противоположном порядке от того, как они происходили исторически). В любом случае для решения некоторых из этих проблем существует eval-when, который может указывать, что некоторый код оценивается во время выполнения или при макрорасширении-времени. В Elisp вы получаете это с eval-when-compile, но в CL вы получаете гораздо больше волос, с несколькими другими "* -time ". (CL также имеет время чтения и имеет ту же среду, что и все остальное, втрое забавляет.) Даже если это кажется хорошей идеей, вы должны прочитать вокруг и посмотреть, как некоторые lispers потерять волосы из-за этого беспорядка.

И на последнем шаге вашего описания вы еще шагнетесь назад во времени и обнаружите что-то, что известно как FEXPR. Я даже не буду указывать на это никаких указаний, вы можете найти массу текстов об этом, почему некоторые люди считают, что это действительно плохая идея, почему некоторые другие люди считают, что это действительно хорошая идея. Практически говоря, эти два "некоторых" являются "самыми" и "немногими" соответственно, хотя некоторые оставшиеся опорные пункты FEXPR могут быть вокальными. Чтобы перевести все это: это взрывчатое вещество... Задавать вопросы об этом - это хороший способ получить длинные огни. (В качестве недавнего примера серьезного обсуждения вы можете увидеть начальный период обсуждения для R7RS, где FEXPRs подошли и привели к точно таким пламенам.) Независимо от того, на какой стороне вы решите сидеть, одно можно сделать очевидным: язык с FEXPRs сильно отличается от языка без них. [Кстати, работа над реализацией в Haskell может повлиять на ваше мнение, поскольку у вас есть место для использования в здравом статическом мире для кода, поэтому соблазн в "симпатичных" сверхдинамичных языках, вероятно, больше...]

Последнее замечание: поскольку вы делаете что-то подобное, вы должны изучить аналогичный проект по реализации схемы в Haskell - IIUC, он даже имеет гигиенические макросы.

Ответ 2

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

Основное отличие заключается в том, что макросы позволяют вам разорвать ссылочную прозрачность; в частности, макрос может изучить код и, следовательно, может различать (3 + 4) и 7, так что обычный код не может. Вот почему макросы являются более мощными и более опасными; большинство программистов были бы расстроены, если бы обнаружили, что (f 7) произвел один результат и (f (+ 3 4)) произвел другой результат.

Ответ 3

Рамблирование фона

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

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

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

Разумный подход заключается в том, чтобы эта поздняя связывающая макросистема для интерпретируемого кода, но "регулярная" (из-за отсутствия лучшего слова) макросистема для скомпилированного кода.

Расширение макросов не требует отдельной среды. Это не должно быть так, потому что локальные макросы должны находиться в том же пространстве имен, что и переменные. Например, в Common Lisp, если мы делаем это (let (x) (symbol-macrolet ((x 'foo)) ...)), макрос внутреннего символа затеняет внешнюю лексическую переменную. Макрообъемник должен знать о формах привязки переменных. И наоборот! Если для переменной x существует внутренняя let, она затеняет внешний symbol-macrolet. Макро расширитель не может просто слепо заменить все вхождения x, которые происходят в теле. Другими словами, макро-расширение Lisp должно знать всю лексическую среду, в которой сосуществуют макросы и другие типы привязок. Конечно, во время макрорасширения вы не создаете среду таким же образом. Конечно, если есть (let ((x (function)) ..), (function) не вызывается и x не получает значения. Но расширитель макросов знает, что в этой среде есть x, поэтому вхождения x не являются макросами.

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

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

Конкретный пример

Предположим, что у нас есть этот код:

(symbol-macrolet ((x (+ 2 2)))
   (print x)
   (let ((x 42)
         (y 19))
     (print x)
     (symbol-macrolet ((y (+ 3 3)))
       (print y))))

Эффект этого на печать 4, 42 и 6. Пусть используется CLISP-реализация Common Lisp и расширяет его с помощью CLISP-специфичной для реализации функции, называемой system::expand-form. Мы не можем использовать стандартный стандартный macroexpand, потому что он не будет возвращаться в локальные макросы:

(system::expand-form   
  '(symbol-macrolet ((x (+ 2 2)))
     (print x)
     (let ((x 42)
           (y 19))
       (print x)
       (symbol-macrolet ((y (+ 3 3)))
         (print y)))))

-->

(LOCALLY    ;; this code was reformatted by hand to fit your screen
  (PRINT (+ 2 2))
  (LET ((X 42) (Y 19))
    (PRINT X)
    (LOCALLY (PRINT (+ 3 3))))) ;

(Теперь, во-первых, об этих формах locally. Почему они там? Обратите внимание, что они соответствуют местам, где у нас есть symbol-macrolet. Это, вероятно, ради деклараций. Если тело symbol-macrolet форма имеет декларации, они должны быть привязаны к этому телу, и locally сделает это. Если расширение symbol-macrolet не оставляет этой упаковки locally, тогда объявления будут иметь неправильную область.)

Из этого расширения макросов вы можете увидеть, что это за задача. Расширитель макросов должен пройти код и распознать все конструкторы связывания (на самом деле все специальные формы), а не только привязывающие конструкции, связанные с макросистемой.

Обратите внимание, что один из экземпляров (print x) оставлен в покое: тот, который находится в области (let ((x ..)) ...). Другой стал (print (+ 2 2)) в соответствии с макросом символа для x.

Еще одна вещь, которую мы можем извлечь из этого, заключается в том, что расширение макросов просто заменяет расширение и удаляет формы symbol-macrolet. Таким образом, остающаяся среда является исходной, минус весь макроматериал, который очищается в процессе расширения. Макрообъем награждает все лексические привязки в одной большой "Великой унифицированной" среде, но затем любезно испаряется, оставляя за собой только код типа (print (+ 2 2)) и другие пережитки, такие как (locally ...), с просто конструкции, не связанные с макросвязью, что приводит к уменьшенной версии исходной среды.

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

Ответ 4

Что вам не хватает, так это то, что эта симметрия ломается, когда вы отдельный анализ из оценки, что и делает все практические реализации Lisp, Расширение макроса произойдет во время фазы анализа, поэтому eval может быть прост.

Ответ 6

Для чего это стоит, схема RS 5 RS Конструкции связывания для синтаксических ключевых слов говорит об этом:

Let-syntax и letrec-syntax аналогичны let и letrec, но они связывают синтаксические ключевые слова с макротрансформаторами вместо привязки переменных к местоположениям, которые содержат значения.

Смотрите: http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-7.html#%_sec_4.3.1

Это означает, что следует использовать отдельную стратегию, по крайней мере, для макросистемы syntax-rules.


Вы можете написать какой-нибудь... интересный код на Схеме, который использует отдельные "места" для макросов. Не имеет смысла смешивать макросы и переменные с одним и тем же именем в любом "реальном" коде, но если вы просто хотите попробовать его, рассмотрите этот пример из схемы куриных:
#;1> let
Error: unbound variable: let
#;1> (define let +)
#;2> (let ((talk "hello!")) (write talk))
"hello!"
#;3> let
#<procedure C_plus>
#;4> (let 1 2)
Error: (let) not a proper list: (let 1 2)

    Call history:

    <syntax>                (let 1 2)       <--
#;4> (define a let)
#;5> (a 1 2)
3