Пожалуйста, объясните некоторые моменты Пола Грэма на Лиспе. - PullRequest
142 голосов
/ 26 апреля 2010

Мне нужна помощь в понимании некоторых моментов из того, что отличало Лиспа от Пола Грэма .

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

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

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

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

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

Ответы [ 4 ]

97 голосов
/ 26 апреля 2010

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

По пунктам (3) и (4):

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

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

;; 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+" (подробнее об этом ниже). Некоторые Лиспы добавляют свои собственные небольшие повороты к этому процессу, но Пол Грэм в основном имел в виду 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, в значительной степени зависит от кода, представляемого регулярным Данные Лиспа, что-то, что разрешено (3). Кстати, здесь действительно важен только аспект «древовидной структуры» кода - возможно, вы могли бы написать Lisp с использованием XML.

64 голосов
/ 26 апреля 2010

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

(defun print-twice (it)
  (print it)
  (print 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))

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

(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?

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

Разница:

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

32 голосов
/ 26 апреля 2010

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

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

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

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

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

0 голосов
/ 22 февраля 2011

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

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

...