it-swarm.com.ru

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

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

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

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

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

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

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

142
unj2

Объяснение Мэтта совершенно нормально - и он делает снимок сравнения 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+" (подробнее об этом ниже) , Некоторые Лиспы добавляют свои небольшие изменения в этот процесс, но Пол Грэм в основном имел в виду Common LISP. По вопросам, касающимся вашего вопроса, Clojure похож на CL.

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

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

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

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

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

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

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

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

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

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

97
Michał Marczyk

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

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

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

Различия:

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

64
Rainer Joswig

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

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

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

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

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

32
Matt Curtis