Пожалуйста, объясните некоторые пункты Пола Грэма на Lisp

Мне нужна помощь, чтобы понять некоторые из соображений от Paul Grahams What Made Lisp Разное.

  • Новая концепция переменных. В Lisp все переменные являются фактически указателями. Значения - это то, что имеют типы, а не переменные, а назначающие или связывающие переменные означают указатели на копирование, а не то, на что они указывают.

  • Тип символа. Символы отличаются от строк тем, что вы можете проверить равенство, сравнив указатель.

  • Обозначение для кода с использованием деревьев символов.

  • Весь язык всегда доступен. Не существует реального различия между временем чтения, временем компиляции и временем выполнения. Вы можете компилировать или запускать код во время чтения, чтения или запуска кода во время компиляции, а также читать или компилировать код во время выполнения.

Что означают эти точки? Как они различаются в таких языках, как C или Java? Существуют ли какие-либо другие языки, кроме Lisp семейных языков, какие-либо из этих конструкций?

Ответ 1

Матовое объяснение прекрасно - и он делает попытку сравнения с C и Java, чего я не буду делать - но по какой-то причине мне очень нравится обсуждать эту тему время от времени, здесь мой выстрел в ответ.

В точках (3) и (4):

Баллы (3) и (4) в вашем списке кажутся наиболее интересными и актуальными сейчас.

Чтобы понять их, полезно иметь четкое представление о том, что происходит с кодом Lisp, в виде потока символов, введенных программистом, на его пути к выполнению. Позвольте использовать конкретный пример:

;; a library import for completeness,
;; we won't concern ourselves with it
(require '[clojure.contrib.string :as str])

;; this is the interesting bit:
(println (str/replace-re #"\d+" "FOO" "a123b4c56"))

Этот фрагмент кода Clojure выводит aFOObFOOcFOO. Обратите внимание, что Clojure, возможно, не полностью удовлетворяет четвертую точку в вашем списке, поскольку время чтения на самом деле не открыто для кода пользователя; Я буду обсуждать, что это означало бы для этого иначе.

Итак, предположим, что у нас есть этот код в файле где-то, и мы просим Clojure выполнить его. Кроме того, позвольте предположить (ради простоты), что мы сделали это мимо импорта библиотеки. Интересный бит начинается с (println и заканчивается на ) вправо. Это лексируется/анализируется, как и следовало ожидать, но уже возникает важная точка: результат - это не какое-то специальное специфическое для компилятора AST представление - это просто регулярная структура данных Clojure/Lisp, а именно вложенный список, содержащий кучу символов, строк и - в этом случае - единственный скомпилированный объект шаблона регулярного выражения, соответствующий литералу #"\d+" (подробнее об этом ниже). Некоторые Lisp добавляют свои собственные небольшие завихрения к этому процессу, но Пол Грэм в основном ссылался на Common Lisp. В отношении вопросов, относящихся к вашему вопросу, Clojure похож на CL.

Весь язык во время компиляции:

После этого момента весь компилятор имеет дело с (это также верно для интерпретатора Lisp; код Clojure всегда бывает скомпилирован) - это структуры данных Lisp, которые программисты Lisp используются для манипулирования, На данный момент появляется замечательная возможность: почему бы не позволить программистам Lisp писать Lisp функции, которые манипулируют данными Lisp, представляющими программы Lisp, и выводят преобразованные данные, представляющие преобразованные программы, которые будут использоваться вместо оригиналов? Другими словами - почему бы не позволить программистам Lisp регистрировать свои функции в виде компиляторов-подпрограмм, называемых макросами в Lisp? И действительно, любая достойная система Lisp имеет такую ​​емкость.

Таким образом, макросы являются регулярными функциями Lisp, работающими над представлением программы во время компиляции, перед заключительной фазой компиляции при испускании фактического объектного кода. Поскольку нет ограничений на типы макросов кода, которые можно запускать (в частности, код, который они запускают, часто сам записывается с использованием макроса с либеральным использованием), можно сказать, что "весь язык доступен во время компиляции".

Весь язык во время чтения:

Вернемся к этому литералу #"\d+" regex. Как упоминалось выше, это преобразуется в фактический скомпилированный объект шаблона во время чтения, прежде чем компилятор услышит первое упоминание о новом коде, который готов к компиляции. Как это происходит?

Ну, путь Clojure в настоящее время реализован, картина несколько отличается от того, что имел в виду Пол Грэхем, хотя возможно с помощью умного взлома. В Common Lisp история будет немного более понятной концептуально. Основы, однако, аналогичны: Lisp Reader - это конечный автомат, который помимо выполнения состояний переходит и в конечном итоге объявляет, достиг ли он "принимающего состояния", выплевывает структуры данных Lisp, которые представляют символы. Таким образом, символы 123 становятся числом 123 и т.д. Важный момент сейчас: этот конечный автомат может быть изменен с помощью кода пользователя. (Как отмечалось ранее, это полностью верно в случае CL, для Clojure требуется хак (обескураженный и не используемый на практике). Но я отвлекся, это статья PG, которую я должен разрабатывать, поэтому... )

Итак, если вы программист Common Lisp, и вам нравится идея векторных литералов Clojure, вы можете просто подключить к читателю функцию для адекватной реакции на некоторую последовательность символов - [ или #[ возможно - и рассматривать его как начало векторного литерала, заканчивающегося при сопоставлении ]. Такая функция называется макросом читателя и точно так же, как обычный макрос, он может выполнять любой вид кода Lisp, включая код, который сам был написан с фанкойльной нотацией, активированной ранее зарегистрированными макросами считывателя. Таким образом, весь язык в режиме чтения для вас.

Обертка:

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

Обратите внимание, что тот факт, что Lisp удовлетворяет точке (3) из вашего списка, очень важен для того, как ему удается удовлетворять точке (4) - особый вкус макросов, предоставляемых Lisp, сильно зависит от кода представляются регулярными данными Lisp, что можно сделать с помощью (3). Кстати, здесь очень важен только "древовидный" аспект кода - вы могли бы иметь Lisp, написанный с использованием XML.

Ответ 2

1) Новая концепция переменных. В Lisp все переменные являются эффективными указателями. Значения - это типы, а не переменные, а назначающие или связывающие переменные - это указатели на копирование, а не то, на что они указывают.

(defun print-twice (it)
  (print it)
  (print it))

'it' является переменной. Он может быть привязан к ЛЮБОМ значению. Нет ограничений и нет типа, связанного с переменной. Если вы вызываете функцию, аргумент не нужно копировать. Переменная похожа на указатель. Он имеет доступ к значению, привязанному к переменной. Нет необходимости резервировать память. Мы можем передать любой объект данных при вызове функции: любой размер и любой тип.

Объекты данных имеют "тип", и все объекты данных могут быть запрошены для своего "типа".

(type-of "abc")  -> STRING

2) Тип символа. Символы отличаются от строк тем, что вы можете проверить равенство, сравнив указатель.

Символ - это объект данных с именем. Обычно имя можно найти для поиска объекта:

|This is a Symbol|
this-is-also-a-symbol

(find-symbol "SIN")   ->  SIN

Поскольку символы являются реальными объектами данных, мы можем проверить, являются ли они одним и тем же объектом:

(eq 'sin 'cos) -> NIL
(eq 'sin 'sin) -> T

Это позволяет нам, например, написать предложение с символами:

(defvar *sentence* '(mary called tom to tell him the price of the book))

Теперь мы можем подсчитать количество символов в предложении:

(count 'the *sentence*) ->  2

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

3) Обозначение для кода с использованием деревьев символов.

Lisp использует свои основные структуры данных для представления кода.

Список (* 3 2) может быть как данными, так и кодом:

(eval '(* 3 (+ 2 5))) -> 21

(length '(* 3 (+ 2 5))) -> 3

Дерево:

CL-USER 8 > (sdraw '(* 3 (+ 2 5)))

[*|*]--->[*|*]--->[*|*]--->NIL
 |        |        |
 v        v        v
 *        3       [*|*]--->[*|*]--->[*|*]--->NIL
                   |        |        |
                   v        v        v
                   +        2        5

4) Весь язык всегда доступен. Не существует реального различия между временем чтения, временем компиляции и временем выполнения. Вы можете компилировать или запускать код во время чтения, чтения или запуска кода во время компиляции, а также читать или компилировать код во время выполнения.

Lisp предоставляет функции READ для чтения данных и кода из текста, LOAD для загрузки кода, EVAL для оценки кода, COMPILE для компиляции кода и PRINT для записи данных и кода в текст.

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

Как они различаются в таких языках, как C или Java?

Эти языки не предоставляют символы, код как данные или оценку времени выполнения данных как код. Объекты данных в C обычно являются нетипизированными.

Существуют ли какие-либо другие языки, кроме Lisp семейных языков, какие-либо из этих конструкций?

У многих языков есть некоторые из этих возможностей.

Разница:

В Lisp эти возможности разработаны на языке так, что они просты в использовании.

Ответ 3

Для точек (1) и (2) он говорит исторически. Переменные Java практически одинаковы, поэтому вам нужно вызвать .equals() для сравнения значений.

(3) говорит о S-выражениях. В этом синтаксисе написаны программы Lisp, что дает множество преимуществ по сравнению с синтаксисом ad-hoc, например Java и C, например, захват повторяющихся шаблонов в макросах гораздо более чистым способом, чем макросы C или С++, а также управление кодом с тем же которые вы используете для данных.

(4), взяв C, например: язык на самом деле является двумя разными субязыками: такими, как if() и while(), и препроцессором. Вы используете препроцессор для сохранения необходимости повторять себя все время или пропустить код С# if/# ifdef. Но оба языка довольно раздельны, и вы не можете использовать while() во время компиляции, как вы можете #if.

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

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

Ответ 4

Точки (1) и (2) также подходят для Python. Взяв простой пример "a = str (82.4)", интерпретатор сначала создает объект с плавающей запятой со значением 82.4. Затем он вызывает конструктор строк, который затем возвращает строку со значением "82.4". "A" с левой стороны - всего лишь метка для этого строкового объекта. Первоначальный объект с плавающей запятой был собран мусором, потому что больше нет ссылок на него.

В схеме все рассматривается как объект аналогичным образом. Я не уверен в Common Lisp. Я бы постарался не думать о понятиях C/С++. Они замедляли меня, когда я пытался разглядеть красивую простоту Lispы.