Гомоиконный и неограниченный самомодифицирующийся код + Действительно ли lisp самомодифицируется?

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

Истинно гомоиконические и самомодифицируемые языки

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

Я нашел только три примера, которые соответствуют этим критериям:

  1. Машинный код. Гомоиконично в том смысле, что все есть числа. Неограниченно модифицируемый, поскольку он включает указатели, которые можно использовать для управления любым адресом памяти, независимо от того, содержит ли этот адрес код или данные.
  2. Мальболге. Те же рассуждения, что и машинный код. Каждая инструкция модифицируется после выполнения
  3. ДНК. Не язык программирования, но все же интересно. Он не самомодифицируется в том же смысле, что и машинный код; Где фактические инструкции + данные изменены на месте. Однако он самовоспроизводится и может видоизменяться / развиваться в соответствии со своим предыдущим состоянием (с такими побочными эффектами, как радиация, которые время от времени его портят). В любом случае это всего лишь косвенный способ самомодификации. Короче говоря, ДНК может самомодифицироваться, но это происходит за счет воспроизведения себя во всей своей полноте вместе с соответствующими мутациями. Физическая цепочка ДНК «неизменна».

Почему Lisp нет в этом списке

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

(+ 1 2 3)

который будет делать то же самое, что и

(eval '(+ 1 2 3))

В первой версии (+ 1 2 3) - это необработанный код, а во второй версии - это данные. Допуская истинность этого утверждения, можно утверждать, что Lisp даже не гомиконичен. Код имеет то же представление, что и данные, в том смысле, что они оба являются списками / деревьями / S-выражениями. Но тот факт, что вы должны явно указать, какие из этих списков / деревьев / S-выражений являются кодом, а какие являются данными, мне кажется, говорит о том, что Lisp в конце концов не гомиконичен. Представления очень похожи, но они отличаются крошечными деталями, которые вы должны сказать, имеете ли вы дело с кодом или данными. Это ни в коем случае не плохо (на самом деле все остальное было бы безумием), но подчеркивает разницу между Лиспом и машинным кодом. В машинном коде вам не нужно явно указывать, какие числа являются инструкциями, какие - указателями, а какие - данными. Все является просто числом до тех пор, пока на самом деле не потребуется интерпретация, и тогда это может быть любое из этих значений.

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

'(+ 1 2 3)

to

'(+ 1 4 3)

А потом прогоняешь через eval. Но когда вы это делаете, вы просто компилируете код и запускаете его. Вы не изменяете существующий код, вы просто создаете и запускаете новый код. C # может делать то же самое, используя деревья выражений, даже в менее удобном формате (что возникает из-за того, что код C # имеет другое представление по сравнению с его AST, в отличие от Lisp, который является собственным AST ). Можете ли вы взять весь исходный файл и начать изменять весь исходный файл во время его выполнения, при этом изменения, внесенные в исходный файл, будут влиять на поведение программы в реальном времени?

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

Способы сделать Lisp гомоиконным / неограниченно самомодифицируемым

Я вижу 3 возможных способа сделать Лисп таким же гомоиконным / самомодифицируемым, как машинный код.

  1. Архитектура, отличная от фон-Неймана. Если бы кто-нибудь мог изобрести какую-нибудь удивительную гипотетическую машину, в которой нижний уровень представления программ - это AST, который может выполняться напрямую (дальнейшая компиляция не требуется). На такой машине AST будет представлять как исполняемые инструкции, так и данные. К сожалению, проблема не будет решена, потому что AST по-прежнему должен быть кодом или данными. Наличие функции eval этого не меняет. В машинном коде вы можете переключаться между кодом и данными сколько угодно. В то время как с eval и Lisp после того, как вы «превратили» некоторый список из данных в код и выполнили его, нет никакого способа вернуть этот список как данные снова. Фактически, этот список исчез навсегда и был заменен его значением. Нам будет не хватать чего-то важного, а именно указателей.
  2. Ярлыки списков. Если бы требовалось, чтобы у каждого списка была уникальная метка, можно было бы выполнить непрямое самомодификацию, запустив функции для списка с заданной меткой. В сочетании с продолжениями это, наконец, позволило бы реализовать самомодифицирующийся код в том же смысле, в каком он есть в машинном коде. Этикетки эквивалентны адресам памяти машинного кода. В качестве примера рассмотрим программу на Лиспе, в которой верхний узел AST имеет метку «main». Затем внутри main вы можете выполнить функцию, которая принимает метку, целое число, атом и копирует атом в список с меткой, соответствующей метке, предоставленной функции, по индексу, заданному целым числом. Затем просто позвоните с текущим продолжением на main. Итак, самомодифицирующийся код.
  3. Макросы Лиспа. Я не нашел времени, чтобы понять макросы Лиспа, и на самом деле они могут делать именно то, о чем я думаю.

Пункт 1. в сочетании с 2. приведет к полностью самомодифицирующемуся Лиспу. При условии, что описанная волшебная машина на Лиспе может быть произведена. 2. сам по себе может создать самомодифицирующийся Лисп, однако реализация на архитектуре фон Неймана может быть крайне неэффективной.

Вопросы

  1. Могут ли какие-либо языки, кроме машинного кода, ДНК и malbolge, выполнять полную само-модификацию, и являются ли они гомоиконными?
  2. (НЕ утруждайтесь отвечать, если вы сделали tl; dr по приведенному выше тексту). Lisp действительно гомиконный + самомодифицирующийся? Если вы так говорите, можете ли вы процитировать, где именно в моем аргументе я заблудился?

Приложение

Языки с неограниченной самомодификацией, но без гомиконности

  1. Сборка. В коде используются слова, а не числа, поэтому он теряет гомиконность, но по-прежнему имеет указатели, которые сохраняют полный контроль над памятью и допускают неограниченную самомодификацию.
  2. Любой язык, использующий необработанные указатели. Например, C / C ++ / Objective C. Тот же аргумент, что и Assembly.
  3. Языки JIT, которые включают виртуальные указатели. Например, C # /. Net работает в небезопасном контексте. Тот же аргумент, что и Assembly.

Другие концепции и языки, которые могут быть как-то актуальны / интересны: Lisp, Ruby, Snobol, Forth и его метапрограммирование во время компиляции, Smalltalk и его отражение, нетипизированное лямбда-исчисление с его свойством, что все является функцией (что подразумевает, что предполагая, что мы мог бы изобрести машину, которая непосредственно выполняет лямбда-исчисление, лямбда-исчисление будет гомоиконным, а машинный код фон Неймана не будет работать на указанной машине. [И теорема Годельса будет выполнима. Ха-ха, страшная мысль: P])


person TheIronKnuckle    schedule 13.12.2011    source источник
comment
Просмотрите это обсуждение: lukego.livejournal.com/18214.html   -  person Vsevolod Dyomkin    schedule 13.12.2011
comment
См. Также stackoverflow.com/a/6480923/271324 для примера в Emacs lisp.   -  person danlei    schedule 14.12.2011
comment
Не знаю, ожидаете ли вы этого, но вы можете найти ядро ​​ быть очень интересным языком программирования.   -  person p4bl0    schedule 31.12.2011
comment
Форма quote не имеет большого значения, поскольку ее можно получить с помощью некоторых других и более простых правил оценки, таких как так называемый явный стиль оценки (тот, который лежит в основе упомянутого выше языка ядра), см. [Shu10]. Самомодифицирующееся свойство также не имеет значения, но это простой факт, что eval (как в явном, так и в неявном стилях) не различает идентичность оцениваемого операнда, поэтому не должно быть заметных различий между модификацией на месте и копией + стиранием- by-eval.   -  person FrankHB    schedule 19.06.2020


Ответы (4)


В первой версии (+ 1 2 3) - это необработанный код, а во второй версии - это данные. Допуская истинность этого утверждения, можно утверждать, что Lisp даже не гомиконичен. Код имеет то же представление, что и данные, в том смысле, что они оба являются списками / деревьями / S-выражениями. Но тот факт, что вы должны явно указать, какие из этих списков / деревьев / S-выражений являются кодом, а какие являются данными, мне кажется, говорит о том, что Lisp в конце концов не гомиконичен.

Это неправда. В первой версии список (+ 1 2 3), который представляет собой данные, передается интерпретатору для выполнения, то есть для интерпретации как кода. Тот факт, что вы должны отмечать s-выражения как код или данные в определенном контексте, не делает Lisp негомиконичным.

Суть гомиконности в том, что все программы - это данные, а не то, что все данные являются программами, поэтому между ними все же есть разница. В Лиспе (1 2 3) - это допустимый список, но не действительная программа, поскольку целое число не является функцией.

[Если мы посмотрим на другой великий гомоиконный язык программирования, Prolog, то увидим тот же феномен: мы можем построить структуру данных foo(X, 1, bar), но без определения foo мы не можем ее выполнить. Кроме того, переменные не могут быть именами предикатов или фактов, поэтому X. никогда не является допустимой программой.]

Lisp в значительной степени самомодифицируется. Например, вот как изменить определение функции:

[1]> (defun foo (x) (+ x 1))
FOO
[2]> (defun bar (x) (+ x 2))
BAR
[3]> (setf (symbol-function 'foo) #'bar)
#<FUNCTION BAR (X) (DECLARE (SYSTEM::IN-DEFUN BAR)) (BLOCK BAR (+ X 2))>
[4]> (foo 3)
5

Объяснение: в [1] мы определили функцию foo как функцию add-1. В [2] мы определили bar как функцию add-2. На [3] мы сбрасываем foo на функцию add-2. На [4] мы видим, что мы успешно изменили foo.

person Fred Foo    schedule 13.12.2011
comment
Суть гомиконности в том, что все программы - это данные, а не то, что все данные - это программы. Об этом раньше не думал. Так какой термин я ищу? В машинном коде он гомиконичен в обоих направлениях. код- ›данные и данные-› код. Я ищу такие языки. Приветствую вас за опровержение моего аргумента твердым опровержением, кстати :) - person TheIronKnuckle; 13.12.2011
comment
Некоторые (многие? Все?) Лиспы также предоставляют интерфейс s-выражения для скомпилированного выражения языка ассемблера / машинного кода / байт-кода (например, (disassemble #'bar)), но я никогда не был (храбрым? Отчаянным?) Достаточно, чтобы попытаться изменить его в этой форме. Я бы сказал, что даже без этой функции гомиконность связана с AST, а не с машинно-зависимой скомпилированной формой. - person BRPocock; 13.12.2011
comment
Лисп так же гомиконичен, как и любой машинный код, в том смысле, что весь код - это данные, но не все данные - это действительный код. Вы можете попробовать выполнить $f00fc7c8 как инструкцию, но нет гарантии, что процессор сможет понять это. - person BRPocock; 13.12.2011
comment
@TheIronKnuckle: в машинном коде, если совокупность данных - это все строки двоичных данных, соответствующие определенной длине слова, тогда не все данные являются программами, по крайней мере, не на всех процессорах. Существуют двоичные строки, которые не образуют допустимые коды операций и могут привести к сбою вашей программы. - person Fred Foo; 13.12.2011
comment
Это неточно. Все данные являются программами, если они синтаксически правильно сформированы. Даже оценщик отклонил бы неправильно сформированные данные как семантически правильные части программы, он все равно должен был принять их как части программы, чтобы попытаться их интерпретировать (и вызвал некоторые ошибки если они семантически неверны), а не ведут к непредсказуемому поведению. То, перенос семантических ошибок на некоторые более ранние этапы (и их синтаксическая обработка) - это деталь реализации в этом контексте. - person FrankHB; 19.06.2020

Лисп - это семейство языков программирования. Члены этого семейства сильно различаются по своим возможностям и методам реализации. Есть две разные интерпретации этого:

  • Лисп - это семейство языков, которые имеют некоторый частично совпадающий набор функций. Это включает в себя все, от первого Lisp до Maclisp, Scheme, Logo, Common Lisp, Clojure и сотни различных языков и их реализаций.

  • У Lisp также есть основная ветвь языков, в названии которых также присутствует слово Lisp: Lisp, MacLisp, Common Lisp, Emacs Lisp, ...

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

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

Common Lisp позволяет реализациям создавать статический код. В то же время он оставляет место для контролируемых модификаций среды выполнения (например, с помощью компилятора времени выполнения, загрузки кода, оценки кода, замены кода и т. Д.).

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

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

Гомоиконность - тоже не очень хорошо определенное понятие. Что касается Лиспа, мне больше нравится, что мы говорим, что исходный код можно превратить в данные, потому что исходный код использует внешнее представление s-выражений, которое является форматом сериализации данных. Но не каждое s-выражение является допустимой программой на Лиспе. Также внутреннее представление исходного кода в виде данных Лиспа никоим образом не является «иконическим». Исходный код Лиспа представлен в виде внешних s-выражений, и после чтения исходного кода он имеет внутреннее представление в виде данных Лиспа. READ читает его, PRINT печатает, а EVAL оценивает его.

В Лиспе также есть другие подходы для предоставления доступа к Лиспу, на котором выполняется ваша программа, и к программе:

  • протокол метаобъектов в CLOS (Common Lisp Object System) является таким примером

  • 3Lisp предоставляет бесконечную башню переводчиков. Ваша программа работает с интерпретатором Лиспа. Этот интерпретатор Лиспа запускается другим интерпретатором Лиспа, который снова работает в другом, и этот тоже ...

person Rainer Joswig    schedule 13.12.2011
comment
Второй 3Lisp. Если вы когда-нибудь хотели довести рефлексивное программирование до крайности, 3Lisp откроет вам глаза. - person Beef; 15.12.2011
comment
Я искал в Google дополнительную информацию о 3Lisp, но мой поиск не дал практически ничего, кроме подтверждения его существования. Есть ли у кого-нибудь актуальные ссылки? - person TheIronKnuckle; 07.09.2015

Макросы можно избежать цитирования, если это то, что вам нужно:

> (defmacro foo (x) (cdr x))
> (foo (+ - 5 2))
3

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

person tsm    schedule 15.12.2011

Здесь я произвел впечатление фаната, и я уже говорил это раньше, но читал о Лиспе Пола Грэма, если вы хотите узнать о макросах Lisp. Они имеют большое значение с точки зрения возможности внесения изменений, которые в противном случае были бы невозможны. Кроме того, я думаю, что здесь важно проводить различие между Lisp, семейством языков, данным Lisp и данной реализацией Lisp.

Я полагаю, что основная проблема, которую я рассматриваю с вашим аргументом, возникает в первом абзаце после «Почему Лиспа нет в этом списке» и имеет отношение к REPL в Лиспе. Когда вы вводите exp (+ 1 2 3) в интерпретатор, вы фактически вызываете функцию EVAL из списка (+ 1 2 3). «Необработанный код», который вы описываете, на самом деле является «данными», передаваемыми в другой код Lisp, это просто данные в определенном контексте.

person jcc333    schedule 24.02.2012