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

146

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

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

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

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

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

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

unj2
источник
10
Я не уверен, что тег функционального программирования здесь оправдан, так как одинаково возможно писать императивный или OO-код во многих Лиспах, так же, как и функциональный код - и на самом деле существует много нефункциональных Лисп код вокруг. Я бы посоветовал вам удалить тег fp и добавить вместо него clojure - надеюсь, это может принести интересную информацию от основанного на JVM Lispers.
Михал Марчик
58
У нас также есть paul-grahamтег здесь? !!! Отлично ...
фактор
@missingfaktor Возможно, ему нужен запрос
кошка,

Ответы:

98

Объяснение Мэтта совершенно прекрасно - и он делает снимок сравнения 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, в значительной степени зависит от кода, представляемого обычными данными Lisp, что-то, что включено (3). Между прочим, здесь действительно важен только аспект кода «древовидная структура» - можно предположить, что Lisp написан с использованием XML.

Michał Marczyk
источник
4
Осторожно: говоря «обычный макрос (компилятор)», вы почти намекаете на то, что макросы компилятора являются «обычными» макросами, когда в Common Lisp (по крайней мере) «макрос компилятора» - это очень специфическая и другая вещь: lispworks. ru / Документация / lw51 / CLHS / Body /…
Кен,
Кен: Хороший улов, спасибо! Я изменю это на «обычный макрос», который, я думаю, вряд ли кого-то запутает.
Михал Марчик,
Фантастический ответ. Я узнал больше из этого за 5 минут, чем за часы, гуглив / размышляя над вопросом. Спасибо.
Чарли Флауэрс
Редактировать: Argh, неправильно понял предложение. Исправлено для грамматики (нужен "пэр", чтобы принять мое редактирование).
Татьяна Рачева
S-выражения и XML могут диктовать одни и те же структуры, но XML гораздо более многословен и поэтому не подходит в качестве синтаксиса.
Сильвестр
66

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?

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

Различия:

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

Райнер Йосвиг
источник
33

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

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

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

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

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

Мэтт Кертис
источник
7
Кроме того, этой мощи (и простоте) уже более 50 лет, и ее достаточно легко реализовать, чтобы начинающий программист мог справиться с ней с минимальными рекомендациями и изучить основы языка. Вы не услышите подобное утверждение Java, C, Python, Perl, Haskell и т. Д. Как хороший проект для начинающих!
Мэтт Кертис
9
Я не думаю, что переменные Java вообще похожи на символы Lisp. В Java нет обозначения для символа, и единственное, что вы можете сделать с переменной, это получить ячейку значения. Строки могут быть интернированы, но обычно они не являются именами, поэтому даже не имеет смысла говорить о том, могут ли они быть процитированы, оценены, переданы и т. Д.
Кен,
2
Более 40 лет может быть более точным :), @Ken: Я думаю, он имеет в виду, что 1) непримитивные переменные в java по ссылке, что похоже на lisp и 2) интернированные строки в java похожи на символы в lisp - Конечно, как вы сказали, вы не можете цитировать или оценивать интернированные строки / код в Java, так что они все еще совершенно разные.
3
@Dan - Не уверен, когда была составлена ​​первая реализация, но первоначальная статья Маккарти о символьных вычислениях была опубликована в 1960 году.
Inaimathi
Java имеет частичную / нерегулярную поддержку «символов» в форме Foo.class / foo.getClass () - то есть объект Class <Foo> типа типа является немного аналогичным - как и значения перечисления, для степень. Но очень минимальные тени символа Lisp.
BRPocock
-3

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

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

CyberFonic
источник