Пояснення Метта ідеально - він знімає порівняння з 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+"
буквальне (докладніше про це нижче). Деякі Ліпси додають до цього процесу свої невеликі повороти, але Пол Грем в основному мав на увазі загального Ліса. Що стосується вашого питання, 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, яку я повинен розробляти, тому ...)
Отже, якщо ви звичайний програміст Lisp і вам подобається ідея векторних літералів у стилі Clojure, ви можете просто підключити до читача функцію, щоб відповідним чином реагувати на певну послідовність символів - [
або, #[
можливо, - і ставитись до неї як початок векторного буквального закінчення на збігу ]
. Така функція називається читачем макросом і подібно до звичайного макросу, вона може виконувати будь-який тип Lisp-коду, включаючи код, який сам був написаний з фанк-позначеннями, включеними раніше зареєстрованими читачами макросами. Таким чином, у вас є вся мова в час читання.
Обгортання:
Насправді, все, що було продемонстровано, - це те, що можна виконувати регулярні функції Lisp у час читання або час компіляції; один крок, який потрібно зробити звідси, щоб зрозуміти, як саме читання та компіляція можливі під час читання, компіляції чи запуску, - це зрозуміти, що читання та компіляція самі виконуються функціями Lisp. Ви можете просто зателефонувати read
або eval
в будь-який час прочитати дані Lisp з потоків символів або скласти та виконати код Lisp відповідно. Ось вся мова прямо там, весь час.
Зверніть увагу, як той факт, що Lisp задовольняє пункт (3) у вашому списку, має важливе значення для способу, яким він вдається задовольнити точку (4) - особливий аромат макросів, наданий Lisp, сильно покладається на код, представлений звичайними даними Lisp, що є щось включене в (3). Між іншим, тут важливим є лише аспект коду "дерева-ish" - ви, можливо, могли б писати Lisp, використовуючи XML.