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

Возможно ли реализовать в схеме Common Lisp макросистему?

Надеюсь, это не лишний вопрос.

Как новичок в схеме, я знаю, что макросы syntax-case более мощные, чем альтернатива syntax-rules, за счет нежелательной сложности.

Возможно ли, однако, реализовать схему макросов Lisp в схеме, более мощную, чем syntax-rules, используя syntax-case?

4b9b3361

Ответ 1

Я постараюсь быть кратким - это сложно, поскольку это, как правило, очень глубокая проблема, больше, чем средний уровень SO Q & A... так что это все равно будет довольно долго. Я также постараюсь быть непредвзятым; хотя я пришел с точки зрения Racket, я использовал Common Lisp в прошлом, и мне всегда нравилось использовать макросы в обоих мирах (и в других, на самом деле). Это не будет прямым ответом на ваш вопрос (в крайнем случае это будет просто "да" ), но он сравнивает эти две системы и, надеюсь, это поможет прояснить проблему для людей - особенно людей вне из Lisp (все вкусы), которые задавались вопросом, почему это такое большое дело. Я также опишу, как defmacro может быть реализован в системах с синтаксисом, но только в целом, чтобы все было ясно (и поскольку вы можете просто найти такие реализации, некоторые из которых приведены в комментариях и в других ответах).

Во-первых, ваш вопрос не является излишним - это очень оправдано и (как я подразумевал), одна из вещей, с которыми встречаются новички Lisp для новичков Scheme и Scheme для Lisp.

Во-вторых, очень мелкий, очень короткий ответ - это то, что люди сказали вам: да, возможно реализовать CL defmacro в схеме, которая поддерживает syntax-case, и, как и ожидалось, у вас есть несколько указателей на такие реализации, Иначе, а реализация syntax-case с использованием простого defmacro - более сложная тема, о которой я не буду говорить слишком много; Я просто скажу, что было сделано, только при очень высокой стоимости повторной реализации lambda и других конструкций связывания, что означает, что это в основном повторная реализация нового языка, который вы должны выполнить, если хотите использовать эту реализацию.

И еще одно уточнение: люди, особенно CLers, очень часто сворачивают две вещи, связанные с макросами Scheme: гигиена и syntax-rules. Дело в том, что в R5RS все, что у вас есть, это syntax-rules, что является очень ограниченной системой перезаписи на основе шаблонов. Как и в случае с другими системами перезаписи, вы можете использовать его наивно или полностью использовать переписывания для определения небольшого языка, который затем можно использовать для записи макросов. См. этот текст для известного объяснения того, как это делается. Хотя это возможно сделать, суть в том, что это сложно, вы используете какую-то странную вещь на маленьком языке, которая напрямую не связана с вашим фактическим языком, и это очень далеко от программирования схемы - возможно, еще хуже что использование реализации гигиенических макросов в CL на самом деле не использует простой CL. Короче говоря, можно использовать только syntax-rules, но это в основном в теоретическом смысле, а не то, что вы хотите использовать в "реальном" коде. Главное здесь, что гигиена не означает, что она ограничена syntax-rules.

Однако syntax-rules не предназначался для "макросистемы схемы" - идея всегда заключалась в том, что у вас есть макро-реализация "низкого уровня", которая используется для реализации syntax-rules, но также может реализовать нарушение гигиены макросов - просто, что не было согласия относительно конкретной реализации на низком уровне. R6RS исправляет это путем стандартизации макросистемы "синтаксис-case" (обратите внимание, что я использую "синтаксис-регистр" как имя системы, отличную от syntax-case, которая является основной формой). Как бы говоря, что обсуждение все еще живое, R7RS сделал шаг назад и исключил его, вернувшись к syntax-rules без каких-либо обязательств в низкоуровневой системе, по крайней мере, до тех пор, пока "маленький язык" идет.

Теперь, чтобы понять разницу между двумя системами, лучше всего прояснить разницу между типами, с которыми они имеют дело. С defmacro трансформатор - это в основном функция, которая принимает S-выражение и возвращает S-выражение. S-выражение здесь представляет собой тип, который состоит из кучки литералов (числа, строки, булевы), символы и структуры вложенных в них списков. (Фактические типы используются немного больше, чем это, но этого достаточно, чтобы понять суть.) Дело в том, что это очень простой мир: вы получаете то, что очень конкретно - вы можете на самом деле распечатать входные данные /output и все, что у вас есть. Обратите внимание, что эта система использует символы для обозначения идентификаторов - и символ является чем-то очень конкретным в этом смысле: x - это часть кода, которая имеет только это имя, x.

Однако эта простота стоит дорого: вы не можете использовать ее для гигиенических макросов, так как у вас нет возможности различать два разных идентификатора, которые называются x. Обычный CL-based defmacro имеет некоторые дополнительные биты, которые компенсируют некоторые из них. Один из таких бит - gensym - инструмент для создания "свежих" символов, которые являются неинтерминированными и поэтому гарантированно отличаются от любого другого символа, включая те, которые имеют одинаковое имя. Другим таким битом является аргумент &environment трансформаторам defmacro, который содержит некоторое представление лексической среды местоположения, в котором используется макрос.

Очевидно, что эти вещи усложняют мир defmacro, поскольку он больше не имеет дело с обычными печатными значениями, и поскольку вам нужно знать о каком-либо представлении среды, что делает еще более понятным, что макрос фактически кусок кода, который является компилятором (поскольку эта среда по существу представляет собой некоторый тип данных, с которым обычно сталкивается компилятор, и тот, который сложнее, чем просто S-выражения). Но, как выясняется, их недостаточно для реализации гигиены. Используя gensym, вы можете получить один простой аспект гигиены, сбитый (избегая захвата макрокоманды кода пользователя), но другой аспект (избегая кода макрокоманды кода пользователя) остается открытым. Некоторые люди соглашаются на это, утверждая, что такого рода захват, которого вы можете избежать, достаточно, но когда вы имеете дело с модульной системой, где макрос окружающая среда часто имеет разные привязки, чем те, которые используются в ее реализации, другая сторона становится гораздо важнее.

Переключитесь на синтаксические макросистемы (и с радостью пропустите syntax-rules, который реализуется тривиально с помощью syntax-case). В этой системе идея состоит в том, что если простые символические S-выражения недостаточно выразительны для представления полного лексического знания (т.е. Различия между двумя различными связями, называемыми x), то мы собираемся "обогатить", их и использовать тип данных, который делает. (Обратите внимание, что существуют другие низкоуровневые макросистемы, которые используют разные подходы к предоставлению дополнительной информации, например, явное переименование и синтаксические замыкания.)

Способ, которым это делается, заключается в том, что макропроцессоры являются функциями, которые потребляют и возвращают "синтаксические объекты", которые являются именно таким видом представления. Точнее, эти синтаксические объекты обычно строятся поверх простого символического представления, только завернутые в структуры, которые имеют дополнительную информацию, которые представляют лексическую область. В некоторых системах (особенно в Racket) все обернуто в синтаксические объекты - символы, а также другие литералы и списки. Учитывая это, неудивительно, что легко получить S-выражения из синтаксических объектов: вы просто вытаскиваете символическое содержимое, а если это список, то продолжайте делать это рекурсивно. В синтаксических системах это делается с помощью syntax-e, который реализует аксессор для символического содержимого объекта синтаксиса и syntax->datum, который реализует версии, которые рекурсивно редуцируют результирующий вывод для получения полного S-выражения. В качестве примечания, это грубое объяснение, почему люди Scheme не говорят о том, что привязки представляются в виде символов, а как идентификаторы.

С другой стороны, возникает вопрос, как начать с данного символического имени и построить такой синтаксический объект. Способ, которым это выполняется, заключается в функции datum->syntax, но вместо того, чтобы api указать, как представлена ​​лексическая информация о сфере видимости, функция принимает в качестве синтаксиса объект как первый аргумент и символическое S-выражение как второе аргумент, и он создает объект синтаксиса путем правильной упаковки S-выражения с лексической областью информации, взятой из первой. Это означает, что для нарушения гигиены то, что вы обычно делаете, начинается с предоставленного пользователем объекта синтаксиса (например, формы тела макроса) и использует его лексическую информацию для создания некоторого нового идентификатора типа this, который отображается в том же сфера.

Это краткое описание достаточно, чтобы увидеть, как работают макросы, которые вы показали. Макрос, который @ChrisJester-Young показал просто, принимает объект синтаксиса, разбивает его на необработанное S-выражение с помощью syntax->datum, отправляет его трансформатору defmacro и возвращает S-выражение, затем он использует syntax->datum, чтобы преобразовать результат обратно в объект синтаксиса, используя лексический контекст кода пользователя. Реализация Racket defmacro немного страннее: во время этапа удаления он сохраняет хеш-таблицу, которая сопоставляет полученные S-выражения с их исходными объектами синтаксиса, а на этапе восстановления она обращается к этой таблице, чтобы получить тот же контекст, что и биты код первоначально был. Это делает его более надежной реализацией для некоторых более сложных макросов, но также более полезна в Racket, поскольку объекты синтаксиса имеют в них гораздо больше информации, таких как местоположение источника, свойства и т.д., И эта тщательная реконструкция обычно приводит к выходным значениям (синтаксис объекты), которые сохраняют информацию, которая была у них на пути в макрос.

Для немного более технического введения для программистов defmacro в синтаксическую систему см. запись syntax-case макросов в блоге. Если вы исходите со стороны Схемы, это будет не так полезно, но это все равно будет полезно для выяснения всей проблемы.

Чтобы сделать это ближе к выводу, я должен отметить, что работа с негигиеничными макросами может быть сложной задачей. Более конкретно, существуют различные способы достижения таких привязок, но они различны по-разному, и обычно могут возвращаться и укусить вас, оставляя несколько разных меток зубов в каждом случае. В "истинной" системе defmacro, такой как CL, вы учитесь жить с определенным набором признаков зубов, которые относительно хорошо известны, и поэтому есть вещи, которые вы просто не делаете. В особенности это тип модульного состава языков с разными привязками для тех же имен, которые Racket использует так часто. В синтаксических системах лучше использовать fluid-let-syntax, который используется для "корректировки" значения имени с лексической областью - и совсем недавно это превратилось в "синтаксические параметры". Существует хороший обзор проблем макросов, нарушающих гигиеничность, который включает описание того, как вы можете попытаться решить проблему с помощью только гигиенических syntax-rules, с базовым синтаксисом, с CL-style defmacro и, наконец, с синтаксическими параметрами. Этот текст становится немного более техническим, но его относительно легко читать первые несколько страниц, и если вы это понимаете, то у вас будет очень хорошая картина всей дискуссии. (Существует также более старое сообщение в блоге, которое лучше описано в документе.)

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

Надеюсь, это дает более полную картину проблемы, с точки зрения осознания компромиссов и возможности самим решать, что лучше всего подходит для вас. Я также надеюсь, что это прояснит некоторые источники для обычного пламени: гигиенические макросы, безусловно, не бесполезны, но поскольку новый тип - это больше, чем просто простые S-выражения, вокруг них больше функциональности - и слишком часто мелкие - читая обходчики, приходит к выводу, что "это слишком сложно". Еще хуже - пламя в духе "в мире Схемы люди почти ничего не знают о метапрограммировании": будучи очень мучительно осведомленными о добавленной стоимости, а также о желаемых преимуществах, люди в мире Схемы провели на порядок больше коллективных усилий на предмет. Это прекрасный выбор придерживаться defmacro, если дополнительные обертки вокруг S-выражений слишком сложны для вашего вкуса, но вы должны знать о расходах, связанных с обучением тому, что вы платите, демпинговая гигиена (и против того, что вы получаете обнимая его).

К сожалению, макросы любого вкуса в целом довольно сложны для новичков (возможно, исключая крайне ограниченный syntax-rules), поэтому люди, как правило, оказываются посреди такого пламени, не имея достаточного опыта, чтобы знать ваше левое правильно. В конечном счете, ничто не сравнится с хорошим опытом в обоих мирах, чтобы прояснить компромиссы. (И это из очень конкретного личного опыта: если бы PLT Scheme не переключился на синтаксический случай N лет назад, я бы, вероятно, никогда не беспокоился об этом... Как только они переключались, мне потребовалось много времени, чтобы преобразовать мой код - и только тогда я понял, насколько это здорово, чтобы иметь надежную систему, где никакие имена не "путаются" по ошибке (что приведет к странным ошибкам и обфускации %%__names__).)

(Тем не менее, очень вероятно, что пламя комментариев произойдет...)

Ответ 2

Здесь реализация Guile define-macro. Обратите внимание, что он полностью реализован с помощью syntax-case:

(define-syntax define-macro
  (lambda (x)
    "Define a defmacro."
    (syntax-case x ()
      ((_ (macro . args) doc body1 body ...)
       (string? (syntax->datum #'doc))
       #'(define-macro macro doc (lambda args body1 body ...)))
      ((_ (macro . args) body ...)
       #'(define-macro macro #f (lambda args body ...)))
      ((_ macro transformer)
       #'(define-macro macro #f transformer))
      ((_ macro doc transformer)
       (or (string? (syntax->datum #'doc))
           (not (syntax->datum #'doc)))
       #'(define-syntax macro
           (lambda (y)
             doc
             #((macro-type . defmacro)
               (defmacro-args args))
             (syntax-case y ()
               ((_ . args)
                (let ((v (syntax->datum #'args)))
                  (datum->syntax y (apply transformer v)))))))))))

Guile имеет специальную поддержку Common Lisp -style docstrings, поэтому, если ваша реализация Scheme не использует docstrings, реализация define-macro может быть еще проще:

(define-syntax define-macro
  (lambda (x)
    (syntax-case x ()
      ((_ (macro . args) body ...)
       #'(define-macro macro (lambda args body ...)))
      ((_ macro transformer)
       #'(define-syntax macro
           (lambda (y)
             (syntax-case y ()
               ((_ . args)
                (let ((v (syntax->datum #'args)))
                  (datum->syntax y (apply transformer v)))))))))))

Ответ 3

Вот реализация define-macro из моей Стандартная прелюдия, а также примеры из книги Пола Грэма:

(define-syntax (define-macro x)
  (syntax-case x ()
    ((_ (name . args) . body)
      (syntax (define-macro name (lambda args . body))))
    ((_ name transformer)
      (syntax
       (define-syntax (name y)
         (syntax-case y ()
           ((_ . args)
             (datum->syntax-object
               (syntax _)
               (apply transformer
                 (syntax-object->datum (syntax args)))))))))))

(define-macro (when test . body) `(cond (,test . ,body)))

(define-macro (aif test-form then-else-forms)
  `(let ((it ,test-form))
     (if it ,then-else-forms)))

(define-macro (awhen pred? . body)
  `(aif ,pred? (begin ,@body)))