Homoiconic и "неограниченный" самоизменяющийся код + действительно ли lisp самоизменяется?
Я буду признателен за то, что мои знания Lisp чрезвычайно минимальны. Однако я чрезвычайно интересуюсь языком и планирую начать серьезно изучать его в ближайшем будущем. Мое понимание этих вопросов, без сомнения, неверно, поэтому, если я скажу что-то, что явно неверно, пожалуйста, прокомментируйте и исправьте меня, а не опровергайте.
Истинно гомоиконические и самодиабируемые языки
Я ищу примеры языков программирования, которые поддерживают как гомо-звукообразность (код имеет то же представление, что и данные), так и неограниченную самостоятельную модификацию (неограниченное значение, означающее, что вы можете изменять каждый аспект вашего работающего кода, а не просто генерировать новый код или указатели функций / / делегатов.)
На данный момент я нашел только три примера, которые соответствуют этим критериям:
- Машинный код. Homoiconic в том, что все это число. Неограниченно модифицируется тем, что включает указатели, которые можно использовать для манипулирования любым адресом памяти независимо от того, содержит ли этот адрес код или данные.
- Malbolge. Те же рассуждения, что и машинный код. Каждая инструкция модифицируется после выполнения
- ДНК. Не язык программирования, но все же интересно. Оно не самоизменяется в том же смысле, что и машинный код; Где фактические инструкции + данные изменены на месте. Однако он самовоспроизводится и может видоизменяться / эволюционировать в соответствии с его предыдущим состоянием (с такими побочными эффектами, как радиация, портивая его время от времени). В любом случае, это всего лишь косвенный способ модификации себя. Короче говоря, ДНК может самоизменяться, но она делает это, воспроизводя себя в своей целостности вместе с соответствующими мутациями. Физическая нить ДНК "неизменна".
Почему Лисп нет в этом списке
Lisp отсутствует в этом списке, потому что мне кажется, что Lisp только почти гомоичен и поддерживает только ограниченную самодоминизацию. Вы можете сделать что-то вроде
(+ 1 2 3)
который будет делать то же самое, что и
(eval '(+ 1 2 3))
В первой версии (+ 1 2 3)
это необработанный код, тогда как во второй версии это данные. Предполагая истинность этого утверждения, можно утверждать, что Лисп даже не омичен. Код имеет то же представление, что и данные, в том смысле, что они оба являются списками / деревьями /S-выражениями. Но тот факт, что вы должны явно пометить, какие из этих списков / деревьев /S-выражений являются кодом, а какие являются данными для меня, похоже, говорит о том, что Lisp в конце концов не омиконичен. Представления чрезвычайно похожи, но они отличаются мельчайшими деталями, которые вы фактически должны сказать, имеете ли вы дело с кодом или данными. Это ни в коем случае не плохо (на самом деле все остальное было бы безумием), но оно подчеркивает разницу между Лиспом и машинным кодом. В машинном коде вам не нужно явно отмечать, какие числа являются инструкциями, которые являются указателями, а какие - данными. Все является просто числом, пока на самом деле не требуется интерпретация, и в этот момент это может быть любая из этих вещей.
Это еще более веский аргумент против неограниченной самодиагностики. Конечно, вы можете взять список, который представляет некоторый код и манипулировать им. Например, изменение
'(+ 1 2 3)
в
'(+ 1 4 3)
И тогда вы запускаете это через eval
, Но когда вы делаете это, вы просто компилируете некоторый код и запускаете его. Вы не изменяете существующий код, вы просто излучаете и запускаете новый код. C# может делать то же самое, используя деревья выражений, даже если в менее удобном формате (что возникает из-за того, что код C# имеет другое представление для своего AST, в отличие от Lisp, который является его собственным AST). Можете ли вы на самом деле взять весь исходный файл и начать изменять весь этот исходный файл во время его работы, при этом изменения, внесенные в исходный файл, влияют в реальном времени на поведение программы?
Если нет какого-либо способа сделать это, Лисп не является ни омиконическим, ни самоизменяющимся. (Чтобы отложить спор над определениями, Лисп не гомоичен и не самоизменяется в той же степени, что и машинный код.)
Способы сделать Lisp Homoiconic/Unrestricly самостоятельно изменяемым
Я вижу 3 возможных способа сделать Лисп таким же гомо-звуковым / самодифицируемым, как машинный код.
- Не фон Неймана. Если бы кто-то мог изобрести какую-то удивительную гипотетическую машину, где представление программ самого низкого уровня - это AST, который может быть выполнен напрямую (дальнейшая компиляция не требуется). На такой машине AST будет представлять как исполняемые инструкции, так и данные. К сожалению, проблема не была решена, потому что AST все еще должен быть либо кодом, либо данными. Превосходство функции eval не меняет этого. В машинном коде вы можете переключаться между кодом и данными столько раз, сколько захотите. Принимая во внимание, что с eval и Lisp, как только вы "утащили" какой-то список из данных в код и выполнили его, нет никакого способа вернуть этот список обратно в качестве данных. Фактически, этот список исчез навсегда и был заменен его значением. Мы бы упустили что-то важное, что, как оказалось, является указателями.
- Список ярлыков. Если бы требовалось, чтобы у каждого списка также была уникальная метка, можно было бы сделать косвенную самомодификацию, запустив функции со списком с данной меткой. В сочетании с продолжениями это, наконец, позволило бы самоизменяющийся код в том же смысле, в котором он есть в машинном коде. Метки соответствуют адресам памяти машинного кода. В качестве примера рассмотрим программу на Лиспе, где верхний узел AST имеет метку "main". Внутри main вы можете затем выполнить функцию, которая принимает метку, Integer, Atom и копирует атом в список с меткой, которая соответствует метке, предоставленной функции, по индексу, указанному в Integer. Тогда просто позвоните с текущим продолжением на главную. Там вы идете, самоизменяющийся код.
- Lisp Macros. Я не потратил время, чтобы понять макросы Lisp, и на самом деле они могут делать именно то, о чем я думаю.
Точка 1. в сочетании с 2. создаст полностью самоизменяющийся Лисп. При условии, что описанная волшебная машина Лиспа может быть изготовлена. 2. один может создать самоизменяющийся Лисп, однако реализация на архитектуре фон Неймана может быть крайне неэффективной.
Вопросы
- Существуют ли какие-либо языки, кроме машинного кода, ДНК и malbolge, которые могут полностью изменить себя и являются homoiconic?
- (НЕ ДУМАЙТЕ отвечать, если вы сделали тролль по вышеуказанному тексту). Действительно ли LISP гомоичны + самоизменяются? Если вы так говорите, можете ли вы точно указать, где в моем споре я сбился с пути?
аппендикс
Языки с неограниченным самоизменением, но без омиконичности
- Ассамблея. Код использует слова в отличие от чисел, поэтому теряет гомоничность, но в нем все еще есть указатели, которые сохраняют полный контроль над памятью и допускают неограниченную самодиагностику.
- Любой язык, который использует сырые указатели. Например, C/C++/Objective C. Тот же аргумент, что и Assembly
- JIT языки, которые включают в себя виртуальные указатели. Например, C#/.NET работает в небезопасном контексте. Тот же аргумент, что и сборка.
Другие понятия и языки, которые могут быть как-то актуальны / интересны: Lisp, Ruby, Snobol, Forth и его метапрограммирование во время компиляции, Smalltalk и его отражение, нетипизированное лямбда-исчисление с его свойством, что все является функцией (Какой тип подразумевает, что мы предполагаем, что мы мог бы изобрести машину, которая выполняет лямбда-исчисление напрямую, лямбда-исчисление было бы гомо-логичным, а машинный код фон Неймана не был бы при запуске на указанной машине. [И теорема Годельса была бы выполнимой. Ха-ха, страшная мысль:P])
4 ответа
В первой версии
(+ 1 2 3)
это необработанный код, тогда как во второй версии это данные. Предполагая истинность этого утверждения, можно утверждать, что Лисп даже не омичен. Код имеет то же представление, что и данные, в том смысле, что они оба являются списками / деревьями /S-выражениями. Но тот факт, что вы должны явно пометить, какие из этих списков / деревьев /S-выражений являются кодом, а какие являются данными для меня, похоже, говорит о том, что Lisp в конце концов не омиконичен.
Это неправда. В первой версии список (+ 1 2 3)
, который является данными, подается к интерпретатору, который должен быть выполнен, то есть интерпретирован как код. Тот факт, что вы должны помечать s-выражения как код или данные в определенном контексте, не делает Lisp негомоикональным.
Смысл гомоничности заключается в том, что все программы являются данными, а не то, что все данные являются программами, поэтому между ними все еще существует различие. В Лиспе (1 2 3)
является допустимым списком, но не допустимой программой, поскольку целое число не является функцией.
[Если мы посмотрим на другой замечательный гомологичный язык программирования, Пролог, то увидим то же явление: мы можем построить структуру данных foo(X, 1, bar)
, но без определения foo
Мы не можем выполнить это. Кроме того, переменные не могут быть именами предикатов или фактов, поэтому X.
никогда не является действительной программой.]
Лисп в значительной степени самоизменяется. Например, вот как изменить определение функции:
[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
,
Lisp - это семейство языков программирования. Члены этого семейства сильно различаются по своим возможностям и методам реализации. Есть две разные интерпретации этого:
Lisp - это семейство языков, которые имеют несколько перекрывающихся функций. Это включает в себя все, от первого 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), где большая часть исходной информации была удалена инструментом доставки, это было бы невозможно. Можно было бы иметь один исполняемый файл машинного кода, который лишен большей части гибкости времени выполнения.
Homoiconicity также не очень четко определенная концепция. Для Lisp мне больше нравится то, что мы говорим, что исходный код можно превратить в данные, потому что исходный код использует внешнее представление s-выражений, которое является форматом сериализации данных. Но не каждое s-выражение является допустимой программой на Лиспе. Также внутреннее представление исходного кода в виде данных на Лиспе никоим образом не является "иконическим". Лисп имеет исходный код в виде внешних s-выражений и после прочтения исходного кода имеет внутреннее представление в виде данных Лисп. READ читает, PRINT печатает, а EVAL оценивает.
В Лиспе также есть другие подходы для предоставления доступа к Лиспу, на котором выполняется ваша программа, и к программе:
протокол мета-объекта в CLOS (Common Lisp Object System) является таким примером
3Lisp предоставляет бесконечную башню переводчиков. Ваша программа выполняет интерпретатор Lisp. Этот интерпретатор Lisp запускается другим интерпретатором Lisp, который снова запускается в другом, и этот тоже...
Макросы могут избегать цитирования, если вы ищете:
> (defmacro foo (x) (cdr x))
> (foo (+ - 5 2))
3
Является (+ - 5 2)
код или данные? Во время записи это похоже на данные. После расширения макроса это похоже на код. И если определение foo
было где-то еще, это можно было бы так же легко (неправильно) представить как функцию, и в этом случае (+ - 5 2)
будет рассматриваться как код, который ведет себя как данные, которые ведут себя как код.
Я встречался здесь как фанат, и уже говорил об этом раньше, но прочитайте статью Пола Грэма " На Лиспе", если хотите узнать о макросах Лиспа. Они имеют большое значение с точки зрения разрешения изменений, которые в противном случае были бы невозможны. Кроме того, я думаю, что здесь важно различать семейство языков Lisp, данный Lisp и данную реализацию Lisp.
Я полагаю, что основная проблема, которую я принимаю в связи с вашим аргументом, возникает в первом абзаце после "Почему Lisp отсутствует в этом списке" и связана с REPL в Lisp. Когда вы вводите exp (+ 1 2 3) в интерпретатор, вы фактически вызываете функцию EVAL в списке (+ 1 2 3). "Необработанный код", который вы описываете, на самом деле является "данными", передаваемыми в другой код lisp, это просто данные в определенном контексте.