Существуют ли диалекты без Lisp, которые допускают синтаксическую абстракцию?

Как говорит Ричард Хикки, секретный соус языков Lisp - это способность напрямую манипулировать абстрактным деревом синтаксиса с помощью макросов. Может ли это быть достигнуто на любых языках диалектов без Lisp?

Ответ 1

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

Чтобы привести пример языка, который имеет эту функцию (но не то, что считалось бы макросом), см. OCaml. Он имеет синтаксическую систему расширения, CamlP4, которая по сути является инструментарием расширения компилятора, и она вращается вокруг абстрактного синтаксиса OCaml как наиболее важного цель. Но это все еще не то, что делает соответствующую особенность в Lisps такой большой.

Важной особенностью Lisps является то, что расширения, которые вы используете с помощью макросов, являются частью языка так же, как любая другая синтаксическая форма. Иными словами, когда вы используете что-то вроде if в Lisp, нет никакой разницы в функциональности, реализована ли она как макрос или как примитивная форма. (На самом деле существует незначительная разница: в некоторых случаях важно знать набор примитивных форм, которые не расширяются дальше.) Более конкретно, библиотека Lisp может обеспечивать простые привязки и макросы, что означает, что библиотеки могут расширять язык гораздо интереснее, чем обычные скучные расширения, которые вы получаете на большинстве языков, способных добавлять только простые привязки (функции и значения).

Теперь, рассматриваемый в этом свете, нечто подобное объекту D очень похоже на природу. Но тот факт, что он имеет дело с сырым текстом, а не с АСТ, ограничивает его полезность. Если вы посмотрите на пример на этой странице,

mixin(GenStruct!("Foo", "bar"));

вы можете видеть, как это не похоже на часть языка - чтобы сделать его более похожим на Lisp, вы бы использовали его естественным образом:

GenStruct(Foo, bar);

не требуя ключевого слова mixin, которое отмечает, где используется макрос, нет необходимости в этом !, а идентификаторы указаны как идентификаторы, а не строки. Еще лучше, определение должно быть выражено более естественно, что-то вроде (придумывая какой-то плохой синтаксис здесь):

template expression GenStruct(identifier Name, identifier M1) {
    return [[struct $Name$ { int $M1$; }; ]]
}

Важно отметить, что, поскольку D - статически типизированный язык, АСТ вкрались в это умственное упражнение явным образом - как типы identifier и expression (я предполагаю здесь, что template обозначает это как определение макроса, но ему все еще нужен тип возврата).

В Lisp вы по существу получаете что-то очень близкое к этой функциональности, а не бедное строковое решение. Но вы получаете еще больше - Lisp намеренно каламбурит по основному типу списка и очень просто объединяет АСТ с языком выполнения: AST состоит из символов и списков и других основных литералов (числа, строки, булевы), и все они являются частью языка времени исполнения. Фактически, для этих литералов Lisp делает еще один шаг вперед и использует литералы в качестве своего собственного синтаксиса - например, число 123 (значение, существующее во время выполнения) представлено синтаксисом, который также является number 123 (но теперь это значение, которое существует во время компиляции). Суть этого заключается в том, что макрокомандный код в Lisp имеет гораздо более сложную задачу, чем другие языки называют "макросами". Представьте себе, например, чтобы код примера D создал N int поля в структуре (где N - новый вход в макрос) - для этого потребуется использовать некоторую функцию для перевода строки в число.

Ответ 2

Lisp

Причины LISP являются "специальными"...

Встроенная функциональность очень экономична:

  • Единственными встроенными структурами данных являются атомы или списки
  • Синтаксис реализуется с точки зрения структуры данных списка
  • Очень мало "системных функций"

Он поддерживает функции таким образом, что новые определения функций неотличимы от встроенных функций:

  • Синтаксис вызова идентичен
  • Оценка аргументов может быть полностью контролирована.

Он поддерживает макросы таким образом, что произвольный код LISP всегда может быть определен в терминах специфичного для домена языка:

  • Синтаксис вызова - это как пользовательский синтаксис функции-вызова, который похож на встроенный синтаксис функции-вызова
  • Оценка аргументов полностью управляема
  • Производится генерация кода LISP
  • Макросы оцениваются во время выполнения, поэтому реализация макроса может вызывать существующий код при генерации нового кода

С помощью перечисленных функций вы можете:

  • Повторно выполнить Lisp -within- Lisp, в очень маленьком коде
  • Добавьте любые существующие идиомы программирования таким образом, который неотличим от встроенных функций

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

Другие языки

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

Некоторые примеры:

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

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

Поскольку Javascript является динамическим языком (имена вызовов функций оцениваются во время выполнения), а также потому, что он предоставляет встроенные функции в контексте структур данных, он полностью "рефлексив" и полностью изменен.

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

Lua очень похож на Javascript в большинстве этих способов.

Препроцессор С++ позволяет вам определить свой собственный DSL с несколько похожим синтаксисом на существующие вызовы функций. Он не позволяет вам контролировать оценку (которая является источником множества ошибок, и почему большинство людей говорят C/C++ macros are "Evil"), но она поддерживает несколько ограниченную форму генерации кода.

Поддержка генерации кода в макросах C/С++ ограничена, поскольку макросы оцениваются до компиляции вашего кода и не могут управляться с помощью кода C. Это почти полностью ограничивается текстовой заменой. Это значительно ограничивает тип кода, который может быть сгенерирован.

Функция шаблона С++ достаточно мощная (макросы WRT to C/С++) для синтаксических дополнений к языку. Он может превратить большую часть оценки кода во время выполнения кода во время компиляции, и может делать статические утверждения в существующем коде. Он может ссылаться на существующий код С++ ограниченным образом.

Но мета-программирование шаблонов (TMP) очень громоздко, потому что оно имеет ужасный синтаксис, является очень строго ограниченным подмножеством С++, имеет довольно ограниченную способность генерации кода и не может быть оценено во время выполнения. С++ шаблоны также, возможно, выводят самые сложные сообщения об ошибках, с которыми вы когда-либо сталкивались при программировании:)

Обратите внимание, что это не позволяет мета-программированию шаблонов быть активной областью исследований во многих сообществах. См. Проект boost, большая часть которого посвящена библиотекам поддержки TMP и библиотекам, поддерживающим TMP.

Утиная печать может позволить вам определить синтаксис объектов, который позволяет заменить реализации во время выполнения. Это похоже на то, как Javascript определяет функции на ассоциативных массивах.

Я не могу сказать для Python (так как я не очень хорошо это знаю), но утиная типизация часто более ограничена, чем динамические функции Javascript из-за отсутствия отражательной способности, изменчивости и подверженности функциональности системы с помощью отражающих/изменяемые интерфейсы. Например, типизация С# утка ограничена всеми этими способами.

Ответ 4

Я не уверен, если вы назовете его "синтаксической абстракцией" как таковой, но он, безусловно, может сделать многое из того, что может сделать Lisp: Ключевое слово mixin позволяет вам преобразовать строку в код (гораздо лучше, чем макросы C), которые в сочетании с шаблонами (которые намного лучше, чем на С++), вы можете делать практически все, что захотите.

Ответ 5

Пролог был бы таким языком. Есть много диалектов Prolog. Одна из идей заключается в том, что их базовый строительный блок - это термин (похожий на s-выражение, кодирующий функцию). Существуют синтаксические анализаторы, которые обеспечивают макрообъекты для этого.

Ответ 6

Я бы сказал, что Tcl квалифицируется - ну, в зависимости от того, считаете ли вы Tcl a Lisp или нет.

Стандартные групповые символы { } на самом деле представляют собой только строковый литерал (без переменной интерполяции), а там eval, поэтому вы можете легко определить свой собственный поток управления или циклический синтаксис (и люди часто делают).