Мне нужна помощь в понимании некоторых моментов из книги Пола Грэма « Что отличало Лисп от других» .
Новая концепция переменных. В Лиспе все переменные фактически являются указателями. Значения - это то, что имеет типы, а не переменные, а присвоение или связывание переменных означает копирование указателей, а не того, на что они указывают.
Тип символа. Символы отличаются от строк тем, что вы можете проверить равенство, сравнивая указатель.
Обозначение кода с использованием деревьев символов.
Весь язык всегда доступен. Нет реального различия между временем чтения, временем компиляции и временем выполнения. Вы можете компилировать или запускать код во время чтения, читать или запускать код во время компиляции, а также читать или компилировать код во время выполнения.
Что означают эти точки? Чем они отличаются в языках типа C или Java? Есть ли сейчас какие-либо из этих конструкций, кроме языков семейства Lisp?
paul-graham
тег здесь? !!! Отлично ...Ответы:
Объяснение Мэтта совершенно прекрасно - и он делает снимок сравнения C и Java, чего я не сделаю, - но по какой-то причине мне очень нравится время от времени обсуждать эту самую тему, так что - вот мой снимок в ответ.
По пунктам (3) и (4):
Пункты (3) и (4) в вашем списке кажутся наиболее интересными и актуальными в настоящее время.
Чтобы понять их, полезно иметь четкое представление о том, что происходит с кодом на Лиспе - в виде потока символов, набираемых программистом - на пути к выполнению. Давайте использовать конкретный пример:
Этот фрагмент кода Clojure распечатывается
aFOObFOOcFOO
. Обратите внимание, что Clojure, возможно, не полностью удовлетворяет четвертому пункту в вашем списке, так как время чтения на самом деле не открыто для пользовательского кода; Я буду обсуждать, что бы это значило, если бы все было иначе.Итак, предположим, что у нас где-то есть этот код в файле, и мы просим Clojure выполнить его. Кроме того, давайте предположим (для простоты), что мы сделали это после импорта библиотеки. Интересная часть начинается
(println
и заканчивается)
далеко справа. Это лексировано / проанализировано, как и следовало ожидать, но уже возникает важный момент: результатом является не какое-то специальное представление AST для конкретного компилятора - это просто обычный структуры данных Clojure / Lisp , а именно вложенный список, содержащий кучу символов, строки и - в этом случае - один скомпилированный объект шаблона регулярного выражения, соответствующий#"\d+"
буквальный (подробнее об этом ниже). Некоторые Лиспы добавляют свои маленькие хитрости к этому процессу, но Пол Грэм в основном имел в виду Common Lisp. По вопросам, относящимся к вашему вопросу, Clojure похож на CL.Весь язык во время компиляции:
После этого все, с чем работает компилятор (это также верно для интерпретатора Lisp; код Clojure всегда компилируется) - это структуры данных Lisp, которыми программисты Lisp привыкли манипулировать. В этот момент становится очевидной замечательная возможность: почему бы не позволить программистам на Лиспе писать функции на Лиспе, которые манипулируют данными на Лиспе, представляющими программы на Лиспе, и выводят преобразованные данные, представляющие собой трансформированные программы, для использования вместо оригиналов? Другими словами - почему бы не позволить программистам на Лиспе регистрировать свои функции в качестве плагинов своего рода, называемых макросами в Лиспе? И действительно, любая приличная система Lisp обладает такой способностью.
Таким образом, макросы - это обычные функции Lisp, работающие с представлением программы во время компиляции, перед завершающей фазой компиляции, когда генерируется фактический объектный код. Поскольку нет никаких ограничений на виды макросов кода, которые разрешено запускать (в частности, код, который они запускают, часто сам пишется при свободном использовании средства макросов), можно сказать, что «весь язык доступен во время компиляции ».
Весь язык во время чтения:
Давайте вернемся к этому
#"\d+"
регулярному выражению. Как упомянуто выше, это преобразуется в фактический объект скомпилированного шаблона во время чтения, прежде чем компилятор услышит первое упоминание о новом коде, готовящемся для компиляции. Как это произошло?Что ж, то, как в настоящее время реализован Clojure, картина несколько отличается от того, что имел в виду Пол Грэм, хотя все возможно с умным взломом . В Common Lisp история была бы немного чище концептуально. Основы, однако, аналогичны: Lisp Reader - это конечный автомат, который, в дополнение к выполнению переходов между состояниями и в конечном итоге объявляет, достиг ли он «принимающего состояния», выплевывает структуры данных Lisp, которые представляют символы. Таким образом, символы
123
становятся числом123
и т. Д. Важный момент наступает сейчас: этот конечный автомат может быть изменен с помощью кода пользователя., (Как отмечалось ранее, это полностью верно в случае с CL; для Clojure требуется взлом (не рекомендуется и не используется на практике). Но я отвлекся, это статья PG, над которой я должен работать, поэтому ...)Так что, если вы программист на Common Lisp и вам нравится идея векторных литералов в стиле Clojure, вы можете просто подключить к читателю функцию, чтобы соответствующим образом реагировать на некоторую последовательность символов -
[
или,#[
возможно, - и рассматривать ее как начало векторного литерала, заканчивающееся на совпадении]
. Такая функция называется макросом чтения и, подобно обычному макросу, может выполнять любой код на Лиспе, включая код, который сам был написан с использованием нестандартной нотации, включенной ранее зарегистрированными макросами чтения. Так что для вас есть весь язык.Подводя итоги:
На самом деле, до сих пор было продемонстрировано, что можно запускать обычные функции Lisp во время чтения или компиляции; Один шаг, который нужно сделать здесь, чтобы понять, как чтение и компиляция сами по себе возможны во время чтения, компиляции или выполнения, состоит в том, чтобы понять, что чтение и компиляция сами выполняются функциями Lisp. Вы можете просто позвонить
read
илиeval
в любое время прочитать данные Lisp из потоков символов или скомпилировать и выполнить код Lisp соответственно. Это весь язык прямо здесь, все время.Обратите внимание, что тот факт, что Lisp удовлетворяет пункту (3) из вашего списка, важен для того, как ему удается удовлетворить пункт (4) - особый вид макросов, предоставляемых Lisp, в значительной степени зависит от кода, представляемого обычными данными Lisp, что-то, что включено (3). Между прочим, здесь действительно важен только аспект кода «древовидная структура» - можно предположить, что Lisp написан с использованием XML.
источник
1) Новая концепция переменных. В Лиспе все переменные фактически являются указателями. Значения - это то, что имеет типы, а не переменные, а присвоение или связывание переменных означает копирование указателей, а не того, на что они указывают.
«это» является переменной. Это может быть связано с любым значением. Нет ограничений и нет типов, связанных с переменной. Если вы вызываете функцию, аргумент копировать не нужно. Переменная похожа на указатель. У него есть способ получить доступ к значению, которое связано с переменной. Нет необходимости резервировать память. Когда мы вызываем функцию, мы можем передать любой объект данных: любой размер и любой тип.
Объекты данных имеют тип, и все объекты данных могут быть запрошены для его типа.
2) Тип символа. Символы отличаются от строк тем, что вы можете проверить равенство, сравнивая указатель.
Символ - это объект данных с именем. Обычно имя может быть использовано для поиска объекта:
Поскольку символы являются реальными объектами данных, мы можем проверить, являются ли они одним и тем же объектом:
Это позволяет нам, например, написать предложение с символами:
Теперь мы можем посчитать количество THE в предложении:
В Common Lisp символы не только имеют имя, но также могут иметь значение, функцию, список свойств и пакет. Таким образом, символы могут использоваться для именования переменных или функций. Список свойств обычно используется для добавления метаданных к символам.
3) Обозначение кода с использованием деревьев символов.
Lisp использует свои основные структуры данных для представления кода.
В списке (* 3 2) могут быть как данные, так и код:
Дерево:
4) Весь язык всегда доступен. Нет реального различия между временем чтения, временем компиляции и временем выполнения. Вы можете компилировать или запускать код во время чтения, читать или запускать код во время компиляции, а также читать или компилировать код во время выполнения.
Lisp предоставляет функции READ для чтения данных и кода из текста, LOAD для загрузки кода, EVAL для оценки кода, COMPILE для компиляции кода и PRINT для записи данных и кода в текст.
Эти функции всегда доступны. Они не уходят. Они могут быть частью любой программы. Это означает, что любая программа может читать, загружать, оценивать или печатать код - всегда.
Чем они отличаются в языках типа C или Java?
Эти языки не предоставляют символы, код в качестве данных или оценку данных во время выполнения в виде кода. Объекты данных в C обычно нетипизированы.
Есть ли сейчас какие-либо из этих конструкций, кроме языков семейства LISP?
Многие языки имеют некоторые из этих возможностей.
Различия:
В Лиспе эти возможности встроены в язык, поэтому их легко использовать.
источник
По пунктам (1) и (2) он говорит исторически. Переменные в Java практически одинаковы, поэтому вам нужно вызывать .equals () для сравнения значений.
(3) говорит о S-выражениях. Программы на Лиспе написаны с использованием этого синтаксиса, что обеспечивает множество преимуществ по сравнению со специальным синтаксисом, таким как Java и C, например, захват повторяющихся шаблонов в макросах гораздо более чистым способом, чем макросы C или шаблоны C ++, и манипулирование кодом с одним и тем же списком ядер. операции, которые вы используете для данных.
(4) например, C: язык - это два разных подъязыка: например, if () и while () и препроцессор. Вы используете препроцессор, чтобы избавить вас от необходимости постоянно повторяться или пропустить код с помощью # if / # ifdef. Но оба языка совершенно разные, и вы не можете использовать while () во время компиляции, как #if.
C ++ делает это еще хуже с шаблонами. Ознакомьтесь с несколькими ссылками на метапрограммирование шаблонов, которое обеспечивает способ генерации кода во время компиляции, и для неспециалистов чрезвычайно сложно обернуться. Кроме того, это действительно куча хаков и уловок с использованием шаблонов и макросов, для которых компилятор не может обеспечить первоклассную поддержку - если вы допустите простую синтаксическую ошибку, компилятор не сможет дать вам четкое сообщение об ошибке.
Ну, с Лиспом, у вас есть все это на одном языке. Вы используете тот же материал для генерации кода во время выполнения, как вы учитесь в первый день. Это не означает, что метапрограммирование является тривиальным, но это, безусловно, проще с поддержкой первого класса и компилятором.
источник
Точки (1) и (2) также соответствуют Python. Взяв простой пример «a = str (82.4)», интерпретатор сначала создает объект с плавающей запятой со значением 82.4. Затем он вызывает строковый конструктор, который затем возвращает строку со значением '82 .4 '. «А» в левой части - это просто метка для этого строкового объекта. Первоначальный объект с плавающей запятой был собран мусором, потому что на него больше нет ссылок.
В схеме все воспринимается как объект аналогичным образом. Я не уверен насчет Common Lisp. Я бы постарался не думать в терминах концепций C / C ++. Они притормозили меня, когда я пытался разобраться в прекрасной простоте Лиспса.
источник