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