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

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

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

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

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

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

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


person Community    schedule 25.04.2010    source источник
comment
Я не уверен, что тег функционального программирования здесь оправдан, поскольку во многих Lisp можно писать императивный или объектно-ориентированный код в равной степени, как и писать функциональный код - а на самом деле много нефункционального кода Lisp. Я бы посоветовал вам удалить тег f-p и вместо этого добавить clojure - надеюсь, это может принести интересный вклад от Lispers на основе JVM.   -  person Michał Marczyk    schedule 26.04.2010


Ответы (4)


Объяснение Мэтта прекрасно - и он делает попытку сравнить с 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.

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

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

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

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

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

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

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

Подводя итог:

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

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

person Community    schedule 26.04.2010
comment
Осторожно: говоря обычный (компиляторный) макрос, вы близки к тому, чтобы подразумевать, что макросы компилятора являются обычными макросами, тогда как в Common Lisp (по крайней мере) макрос компилятора - это очень специфическая и другая вещь: lispworks.com/documentation/lw51/CLHS/Body/ - person Ken; 28.04.2010
comment
Кен: Хороший улов, спасибо! Я заменю его на обычный макрос, который, думаю, никого не сбивает с толку. - person Michał Marczyk; 28.04.2010
comment
Фантастический ответ. Я узнал из этого больше за 5 минут, чем за часы, когда гуглил / размышлял над вопросом. Спасибо. - person Charlie Flowers; 05.08.2010
comment
Изменить: argh, неправильно понял продолжение предложения. С поправками на грамматику (нужен коллега, чтобы принять мою правку). - person Tatiana Racheva; 23.06.2011
comment
S-выражения и XML могут диктовать одни и те же структуры, но XML гораздо более подробный и поэтому не подходит в качестве синтаксиса. - person Sylwester; 01.03.2013

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?

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

Различия:

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

person Community    schedule 26.04.2010

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

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

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

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

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

person Community    schedule 25.04.2010
comment
Кроме того, этой мощи (и простоте) уже более 50 лет, и ее достаточно легко реализовать, чтобы начинающий программист мог разобраться с ней с минимальным руководством и изучить основы языка. Вы не услышите подобного заявления о Java, C, Python, Perl, Haskell и т. Д. Как о хорошем проекте для новичков! - person Matt Curtis; 26.04.2010
comment
Я вообще не думаю, что переменные Java похожи на символы Лиспа. В Java нет обозначения для символа, и единственное, что вы можете сделать с переменной, - это получить ее ячейку значения. Строки могут быть интернированы, но обычно это не имена, поэтому нет смысла даже говорить о том, можно ли их цитировать, оценивать, передавать и т. Д. - person Ken; 28.04.2010
comment
Более 40 лет может быть более точным :), @Ken: Я думаю, он имеет в виду, что 1) непримитивные переменные в java создаются по ссылке, что похоже на lisp и 2) интернированные строки в java похожи на символы в lisp - конечно, как вы сказали, вы не можете цитировать или оценивать интернированные строки / код в Java, так что они все равно совсем другие. - person ; 13.06.2010
comment
@Dan - Не уверен, когда была собрана первая реализация, но первоначальная статья Маккарти - person Inaimathi; 20.05.2011
comment
Java действительно имеет частичную / нерегулярную поддержку «символов» в форме Foo.class / foo.getClass () - то есть объект класса ‹Foo› типа типа типа немного аналогичен - как и значения перечисления, для градус. Но очень минимальные тени от символа Лиспа. - person BRPocock; 03.08.2012
comment
Java, будучи языком со статической типизацией и несовершенным собственными типами, которые ведут себя с разными объектами, не удовлетворяет (1). LISP обычно имеют строгую типизацию (они не приводят автоматически значения, как PHP), но также и динамическую типизацию в отличие от статической типизации Java. - person Sylwester; 01.03.2013

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

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

person Community    schedule 22.02.2011