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