Я изучаю Scheme из SICP, и у меня складывается впечатление, что большая часть того, что делает Scheme и, тем более, LISP особенным, - это макросистема. Но, поскольку макросы расширяются во время компиляции, почему люди не делают эквивалентные системы макросов для C / Python / Java / что угодно? Например, можно привязать python
команду к expand-macros | python
или как угодно. Код будет по-прежнему переносимым для людей, которые не используют систему макросов, можно просто развернуть макросы перед публикацией кода. Но я не знаю ничего подобного, кроме шаблонов в C ++ / Haskell, которые, как я понимаю, на самом деле не совпадают. А как насчет LISP, если что-нибудь облегчает внедрение макросистем?
21
Ответы:
Многие Lispers скажут вам, что особенность Lisp - это гомоконичность , что означает, что синтаксис кода представлен теми же структурами данных, что и другие данные. Например, вот простая функция (с использованием синтаксиса схемы) для вычисления гипотенузы прямоугольного треугольника с заданными длинами сторон:
Теперь гомоконичность говорит о том, что приведенный выше код фактически представлен в виде структуры данных (в частности, списков списков) в коде на Лиспе. Итак, рассмотрим следующие списки и посмотрим, как они «склеиваются» вместе:
(define #2# #3#)
(hypot x y)
(sqrt #4#)
(+ #5# #6#)
(square x)
(square y)
Макросы позволяют вам обращаться с исходным кодом как со списком вещей. Каждый из этих 6 «подсписков» содержат либо указатели на другие списки, или символы (в данном примере:
define
,hypot
,x
,y
,sqrt
,+
,square
).Итак, как мы можем использовать гомойконичность, чтобы «отделить» синтаксис и создать макросы? Вот простой пример. Давайте переопределим
let
макрос, который мы назовемmy-let
. Как напоминание,должен расширяться в
Вот реализация, использующая схему «явное переименование» макросов † :
form
Параметр связан с фактической формой, так что для нашего примера, это будет(my-let ((foo 1) (bar 2)) (+ foo bar))
. Итак, давайте проработаем пример:cadr
хватает((foo 1) (bar 2))
часть формы.Затем мы извлекаем тело из формы.
cddr
хватает((+ foo bar))
часть формы. (Обратите внимание, что это предназначено для захвата всех подчиненных форм после привязки; поэтому, если форма былатогда тело будет
((debug foo) (debug bar) (+ foo bar))
.)lambda
выражение и вызываем, используя привязки и тело, которые мы собрали. Обратный удар называется «квазицитатой», что означает трактовать все, что находится внутри квазицитаты, как буквальные данные, кроме битов после запятых («unquote»).(rename 'lambda)
средства использоватьlambda
связывание в силу , когда этот макрос определен , а не то , чтоlambda
связывание может быть вокруг , когда этот макрос используется . (Это называется гигиеной .)(map car bindings)
возвращает(foo bar)
: первый элемент в каждой из привязок.(map cadr bindings)
возвращает(1 2)
: второй элемент в каждой из привязок.,@
делает «соединение», которое используется для выражений, которые возвращают список: это заставляет элементы списка вставляться в результат, а не сам список.(($lambda (foo bar) (+ foo bar)) 1 2)
, где$lambda
здесь упоминается переименованныйlambda
.Прямо, верно? ;-) (Если это не так просто для вас, просто представьте, насколько сложно было бы внедрить систему макросов для других языков.)
Таким образом, у вас могут быть макросистемы для других языков, если у вас есть возможность «разобрать» исходный код без лишних сложностей. Есть несколько попыток в этом. Например, sweet.js делает это для JavaScript.
† Для опытных Schemers, читающих это, я намеренно решил использовать явное переименование макросов в качестве среднего компромисса между
defmacro
s, используемыми другими диалектами Lisp, иsyntax-rules
(что будет стандартным способом реализации такого макроса в Scheme). Я не хочу писать на других диалектах Лисп, но я не хочу отталкивать не-шемеров, которые не привыклиsyntax-rules
.Для справки вот
my-let
макрос, который используетsyntax-rules
:Соответствующая
syntax-case
версия выглядит очень похоже:Разница между ними заключается в том, что ко всему внутри
syntax-rules
применяется неявное#'
применение, поэтому вы можете использовать только пары шаблон / шаблонsyntax-rules
, поэтому оно полностью декларативно. Напротивsyntax-case
, бит после шаблона является действительным кодом, который, в конце концов, должен возвращать объект синтаксиса (#'(...)
), но может содержать и другой код.источник
syntax-rules
которые являются чисто декларативными. Я могу использовать для сложных макросов,syntax-case
который является частично декларативным и частично процедурным. И затем есть явное переименование, которое является чисто процедурным. (Большинство реализаций Схемы будут предоставлять либоsyntax-case
ER, либо ER. Я не видел ни одного, который обеспечивал бы оба. Они эквивалентны по силе.)Особое мнение: гомоконичность Лиспа гораздо менее полезная вещь, чем большинство фанатов Лиспа заставили бы вас поверить.
Чтобы понимать синтаксические макросы, важно понимать компиляторы. Задача компилятора - превратить читаемый человеком код в исполняемый код. С точки зрения очень высокого уровня, у этого есть две общих фазы: синтаксический анализ и генерация кода .
Синтаксический анализ - это процесс чтения кода, интерпретации его в соответствии с набором формальных правил и преобразования его в древовидную структуру, обычно известную как AST (абстрактное синтаксическое дерево). Для всего разнообразия языков программирования это одна замечательная общность: практически каждый язык программирования общего назначения разбирается на древовидную структуру.
Генерация кода принимает AST анализатора в качестве входных данных и преобразует его в исполняемый код посредством применения формальных правил. С точки зрения производительности это гораздо более простая задача; многие высокоуровневые языковые компиляторы проводят 75% или больше своего времени на разборе.
Что нужно помнить о Лиспе, так это то, что он очень, очень старый, Среди языков программирования только FORTRAN старше, чем Lisp. Еще в те дни разбор (медленная часть компиляции) считался мрачным и загадочным искусством. В оригинальных работах Джона Маккарти по теории Лиспа (тогда, когда это была просто идея, которую он никогда не думал, что она может быть реализована как настоящий язык программирования), описывается несколько более сложный и выразительный синтаксис, чем современные «S-выражения везде и везде» обозначение Это произошло позже, когда люди пытались это осуществить. Поскольку синтаксический анализ в то время не был хорошо понят, они в основном наказывали его и сводили синтаксис к гомоиконичной древовидной структуре, чтобы сделать работу синтаксического анализатора совершенно тривиальной. Конечным результатом является то, что вы (разработчик) должны сделать много парсера Работайте для этого, записывая формальный AST прямо в ваш код. Homoiconicity не «делает макросы намного проще», а делает написание всего остального намного сложнее!
Проблема в том, что, особенно с динамической типизацией, S-выражениям очень трудно носить с собой много семантической информации. Когда весь ваш синтаксис относится к одному и тому же типу (списки списков), контекст не обеспечивает много контекста, предоставляемого синтаксисом, и поэтому система макросов имеет очень мало возможностей для работы.
Теория компиляторов прошла долгий путь с 1960-х годов, когда был изобретен Lisp, и хотя достигнутые результаты были впечатляющими для своего времени, сейчас они выглядят довольно примитивно. Для примера современной системы метапрограммирования, взгляните на (к сожалению недооцененный) язык Boo. Boo является статически типизированным, объектно-ориентированным и открытым исходным кодом, поэтому каждый узел AST имеет тип с четко определенной структурой, в которую разработчик макросов может читать код. Язык имеет относительно простой синтаксис, вдохновленный Python, с различными ключевыми словами, которые придают внутренним семантическим значениям встроенные древовидные структуры, а его метапрограммирование имеет интуитивно понятный синтаксис квазиквот для упрощения создания новых узлов AST.
Вот макрос, который я создал вчера, когда понял, что применяю один и тот же шаблон к множеству разных мест в коде GUI, где я вызываю
BeginUpdate()
элемент управления UI, выполняю обновление вtry
блоке и затем вызываюEndUpdate()
:На
macro
самом деле команда - это сам макрос , который принимает тело макроса в качестве входных данных и генерирует класс для обработки макроса. Имя макроса используется в качестве переменной, котораяMacroStatement
заменяет узел AST, представляющий вызов макроса. [| ... |] является блоком квазицитатов, генерирующим AST, который соответствует коду внутри, а внутри блока квазиквот символ $ обеспечивает функцию «unquote», подставляя в узел, как указано.С этим можно написать:
и расширить его до:
Выражая макрос таким образом , является более простым и понятным , чем это было бы в макросе Lisp, потому что разработчик знает структуру
MacroStatement
и знает , какArguments
иBody
свойства работы, и что присущая знания могут быть использованы для выражения понятий , связанных с очень интуитивным путь. Это также безопаснее, потому что компилятор знает структуруMacroStatement
, и если вы попытаетесь закодировать что-то, что недопустимо дляMacroStatement
, компилятор сразу же поймает это и сообщит об ошибке, а вы не узнаете, пока что-то не взорвется во время выполнения.Привить макросы на Haskell, Python, Java, Scala и т. Д. Несложно, поскольку эти языки не гомоичны; это сложно, потому что языки не предназначены для них, и это работает лучше всего, когда иерархия AST вашего языка разработана с нуля для изучения и манипулирования макросистемой. Когда вы работаете с языком, который изначально разрабатывался с учетом метапрограммирования, с макросами работать намного проще и проще!
источник
if...
не похоже, например, на вызов функции. Я не знаю Boo, но представьте, что у Boo не было сопоставления с шаблоном, не могли бы вы представить его с собственным синтаксисом в качестве макроса? Я хочу сказать, что любой новый макрос в Лиспе кажется на 100% естественным, на других языках он работает, но вы можете увидеть стежки.Как же так? Весь код в SICP написан в стиле без макросов. В SICP нет макросов. Только в сноске на странице 373 упоминаются макросы.
Они не обязательно. Lisp предоставляет макросы как в интерпретаторах, так и в компиляторах. Таким образом, может не быть времени компиляции. Если у вас есть интерпретатор Lisp, макросы раскрываются во время выполнения. Поскольку многие системы Lisp имеют встроенный компилятор, можно генерировать код и компилировать его во время выполнения.
Давайте проверим это, используя SBCL, реализацию Common Lisp.
Давайте переключим SBCL на интерпретатора:
Теперь мы определим макрос. Макрос печатает что-то, когда он вызывается для расширенного кода. Сгенерированный код не печатается.
Теперь давайте используем макрос:
Видеть. В приведенном выше случае Лисп ничего не делает. Макрос не раскрывается во время определения.
Но во время выполнения, когда код используется, макрос раскрывается.
Опять же, во время выполнения, когда используется код, макрос раскрывается.
Обратите внимание, что SBCL будет расширяться только один раз при использовании компилятора. Но различные реализации Lisp также предоставляют интерпретаторы, такие как SBCL.
Почему макросы в Лиспе просты? Ну, они не очень легки. Только в Лиспе, и во многих есть встроенная поддержка макросов. Поскольку многие Лиспы поставляются с обширным механизмом для макросов, похоже, это легко. Но макро-механизмы могут быть чрезвычайно сложными.
источник
eval
илиload
программируете на любом языке Lisp, макросы в них тоже будут обрабатываться. Принимая во внимание, что если вы используете систему препроцессора, как предложено в вашем вопросе,eval
и тому подобное, вы не выиграете от расширения макроса.read
в Lisp вызывается parse . Это различие важно, потому чтоeval
работает с фактическими структурами данных списка (как упомянуто в моем ответе), а не с текстовой формой. Таким образом, вы можете использовать(eval '(+ 1 1))
и вернуть 2, но если вы(eval "(+ 1 1)")
, вы вернетесь"(+ 1 1)"
(строка). Вы используетеread
для перехода от"(+ 1 1)"
(строка из 7 символов) к(+ 1 1)
(список с одним символом и двумя фиксированными числами).read
одновременно. Они работают во время компиляции в том смысле, что если у вас есть подобный код(and (test1) (test2))
, он будет расширен(if (test1) (test2) #f)
(в схеме) только один раз, когда код загружен, а не каждый раз, когда код выполняется, но если вы делаете что-то вроде(eval '(and (test1) (test2)))
, который скомпилирует (и расширит макрос) это выражение соответствующим образом во время выполнения.eval
работают только над текстовыми строками, и их возможности для модификации синтаксиса намного более тусклы и / или громоздки.Гомоиконичность значительно упрощает реализацию макросов. Идея, что код - это данные, а данные - это код, позволяет более или менее (за исключением случайного захвата идентификаторов, решаемых гигиеническими макросами ) свободно заменять один другим. Lisp и Scheme облегчают это благодаря их синтаксису S-выражений, которые имеют одинаковую структуру и поэтому легко превращаются в AST, которые составляют основу синтаксических макросов .
Языки без S-выражений или гомойконичности будут сталкиваться с проблемами при реализации синтаксических макросов, хотя это, безусловно, может быть сделано. Project Kepler пытается представить их, например, в Scala.
Самая большая проблема, связанная с использованием макросов синтаксиса, помимо неоднородности, - это проблема произвольно генерируемого синтаксиса. Они предлагают огромную гибкость и мощь, но ценой того, что ваш исходный код может быть не так легко понять или поддерживать.
источник