Как Lisp позволяет Вам переопределить сам язык?

Я услышал, что Lisp позволяет Вам переопределить сам язык, и я попытался исследовать его, но нигде нет никакого четкого объяснения. У кого-либо есть простой пример?

60
задан Rainer Joswig 22 February 2010 в 14:08
поделиться

4 ответа

Пользователи Lisp называют Lisp программируемым языком программирования. Он используется для символьных вычислений - вычислений с помощью символов.

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

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

В традиции Лиспа было много попыток обеспечить эти возможности. Диалект Лиспа и определенная реализация могут предлагать только часть из них.

Несколько способов переопределить/изменить/расширить функциональность, предоставляемую основными реализациями Common Lisp:

  • синтаксис s-выражений. Синтаксис s-выражений не является фиксированным. Читатель (функция READ) использует так называемые таблицы чтения для указания функций, которые будут выполняться при чтении символа. Таблицы чтения можно изменять и создавать. Это позволяет, например, изменять синтаксис списков, символов или других объектов данных. Можно также ввести новый синтаксис для новых или существующих типов данных (например, хэш-таблиц). Также можно полностью заменить синтаксис s-выражений и использовать другой механизм разбора. Если новый синтаксический анализатор возвращает формы Лиспа, то никаких изменений в интерпретаторе или компиляторе не требуется. Типичным примером является макрос чтения, который может читать инфиксные выражения. В таком макросе чтения используются инфиксные выражения и правила старшинства для операторов. Макросы чтения отличаются от обычных макросов: макросы чтения работают на уровне символов синтаксиса данных Лиспа.

  • replacing functions. Функции верхнего уровня привязаны к символам. Пользователь может изменить эту привязку. Большинство реализаций имеют механизм, позволяющий это сделать даже для многих встроенных функций. Если вы хотите предоставить альтернативу встроенной функции ROOM, вы можете заменить ее определение. Некоторые реализации выдадут ошибку, а затем предложат продолжить изменение. Иногда требуется разблокировать пакет. Это означает, что функции в целом могут быть заменены новыми определениями. При этом существуют ограничения. Одно из них заключается в том, что компилятор может инлайнить функции в код. Чтобы увидеть эффект, необходимо перекомпилировать код, который использует измененный код.

  • advising functions. Часто требуется добавить некоторое поведение к функциям. В мире Lisp это называется "советовать". Многие реализации Common Lisp предоставляют такую возможность.

  • custom packages. Пакеты группируют символы в пространствах имен. Пакет COMMON-LISP является домом для всех символов, которые являются частью стандарта ANSI Common Lisp. Программист может создавать новые пакеты и импортировать существующие символы. Поэтому вы можете использовать в своих программах пакет EXTENDED-COMMON-LISP, который предоставляет больше или другие возможности. Просто добавив (IN-PACKAGE "EXTENDED-COMMON-LISP"), вы можете начать разработку, используя свою собственную расширенную версию Common Lisp. В зависимости от используемого пространства имен, диалект Лиспа, который вы используете, может незначительно или даже радикально отличаться. В Genera on the Lisp Machine таким образом бок о бок существуют несколько диалектов Лиспа: ZetaLisp, CLtL1, ANSI Common Lisp и Symbolics Common Lisp.

  • CLOS и динамические объекты. Объектная система Common Lisp поставляется со встроенными изменениями. Протокол Meta-Object Protocol расширяет эти возможности. Сам CLOS может быть расширен/переопределен в CLOS. Вам нужно другое наследование. Напишите метод. Вам нужны разные способы хранения экземпляров. Напишите метод. Слоты должны содержать больше информации. Создайте для этого класс. Сам CLOS разработан таким образом, что он способен реализовать целую "область" различных объектно-ориентированных языков программирования. Типичные примеры - добавление таких вещей, как прототипы, интеграция с чужими объектными системами (например, Objective C), добавление персистентности, ...

  • Лисп-формы. Интерпретация форм Лиспа может быть переопределена с помощью макросов. Макрос может анализировать исходный код, в который он заключен, и изменять его. Существуют различные способы управления процессом преобразования. Сложные макросы используют обходчик кода, который понимает синтаксис форм Лиспа и может применять преобразования. Макросы могут быть тривиальными, но могут быть и очень сложными, как макросы LOOP или ITERATE. Другими типичными примерами являются макросы для встраивания SQL и генерации встроенного HTML. Макросы также могут использоваться для переноса вычислений на время компиляции. Поскольку компилятор сам является программой на языке Lisp, произвольные вычисления могут быть выполнены во время компиляции. Например, макрос Lisp может вычислить оптимизированную версию формулы, если во время компиляции известны определенные параметры.

  • Symbols. Common Lisp предоставляет макросы символов. Макросы символов позволяют изменять значение символов в исходном коде. Типичный пример: (with-slots (foo) bar (+ foo 17)) Здесь символ FOO в исходнике, заключенный в WITH-SLOTS, будет заменен на вызов (slot-value bar 'foo).

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

  • Обработка условий - обработка условий, возникающих в результате использования языка программирования определенным образом. Common Lisp предоставляет расширенный способ обработки ошибок. Система условий также может быть использована для переопределения возможностей языка. Например, можно обрабатывать ошибки неопределенных функций с помощью самописного механизма автозагрузки. Вместо того чтобы выходить в отладчик, когда Лисп видит неопределенную функцию, обработчик ошибок может попытаться автозагрузить функцию и повторить операцию после загрузки необходимого кода.

  • Специальные переменные - введение привязок переменных в существующий код. Многие диалекты Лиспа, например Common Lisp, предоставляют специальные/динамические переменные. Их значение просматривается во время выполнения в стеке. Это позволяет окружающему коду добавлять привязки переменных, которые влияют на существующий код, не изменяя его. Типичным примером является переменная типа *standard-output*. Можно перепривязать переменную, и весь вывод, использующий эту переменную в динамической области действия новой привязки, будет идти в новом направлении. Ричард Столлман утверждал, что это очень важно для него, что это было сделано по умолчанию в Emacs Lisp (хотя Столлман знал о лексическом связывании в Scheme и Common Lisp).

Лисп имеет эти и другие возможности, потому что его использовали для реализации множества различных языков и парадигм программирования. Типичным примером является встроенная реализация языка логики, скажем, Prolog. Lisp позволяет описывать термины Prolog с помощью s-выражений, а с помощью специального компилятора термины Prolog могут быть скомпилированы в код Lisp. Иногда требуется обычный синтаксис Prolog, тогда парсер разбирает типичные термины Prolog в формы Lisp, которые затем компилируются. Другими примерами встроенных языков являются языки, основанные на правилах, математические выражения, термины SQL, встроенный ассемблер Lisp, HTML, XML и многие другие.

86
ответ дан 24 November 2019 в 17:45
поделиться

Я собираюсь указать, что схема отличается от Common Lisp, когда дело касается определения нового синтаксиса. Он позволяет вам определять шаблоны с помощью define-syntax , которые применяются к вашему исходному коду, где бы они ни использовались. Они выглядят так же, как функции, только они запускаются во время компиляции и преобразуют AST.

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

(define-syntax let
  (syntax-rules ()
    [(let ([var expr] ...) body1 body2 ...)
     ((lambda (var ...) body1 body2 ...) expr ...)]))

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

15
ответ дан 24 November 2019 в 17:45
поделиться

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

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

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

4
ответ дан 24 November 2019 в 17:45
поделиться

Этот ответ конкретно касается Common Lisp (далее CL), хотя некоторые части ответа могут быть применимы к другим языкам семейства Lisp.

Поскольку CL использует S-выражения и (в основном) выглядит как последовательность приложений-функций, нет очевидной разницы между встроенными функциями и пользовательским кодом. Основное отличие состоит в том, что «то, что предоставляет язык», доступно в определенном пакете в среде кодирования.

При некоторой осторожности несложно закодировать замены и использовать их вместо них.

Теперь, «нормальный» читатель (часть, которая читает исходный код и преобразует его во внутреннюю нотацию) ожидает, что исходный код будет в довольно специфическом формате (заключенные в скобки S-выражения), но поскольку читатель управляется чем-то, называемым "таблицы чтения", которые могут быть созданы и изменены разработчиком, также возможно изменить внешний вид исходного кода.

Эти две вещи должны, по крайней мере, дать некоторое обоснование того, почему Common Lisp можно рассматривать как перепрограммируемый язык программирования. У меня нет простого примера, но у меня есть частичная реализация перевода Common Lisp на шведский язык (созданная 1 апреля, несколько лет назад).

2
ответ дан 24 November 2019 в 17:45
поделиться
Другие вопросы по тегам:

Похожие вопросы: