Парсер

Clojure является гомоиконическим языком, что является причудливым термином, описывающим тот факт, что программы на Clojure представлены структурами данных Clojure. Это очень важное различие между Clojure (и Common Lisp) и большинством других языков программирования - Clojure определяется в терминах оценки структур данных, а не в терминах синтаксиса потоков символов/файлов. Программы на Clojure зачастую преобразовывают и создают другие программы на Clojure.

Тем не менее, большинство программ Clojure начинают свою жизнь как текстовые файлы, и задача парсера - разобрать текст и создать структуру данных, которую увидит компилятор. Это не просто этап работы компилятора. Парсер и представления данных Clojure в стандартном выводе сами по себе полезны во многих тех же контекстах, в которых можно использовать XML, JSON и т.п.

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

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

Формы парсера

Символы

Литералы

Списки

Списки - это ноль или более форм, заключенных в круглые скобки: (a b c).

Векторы

Векторы - это ноль или более форм, заключенных в квадратные скобки: [1 2 3].

Отображения

Синтаксис пространства имен отображения

Добавлен в Clojure 1.9.

Литералы отображений могут опционально указывать контекст пространства имен по умолчанию для ключей в отображении с помощью префикса #:ns, где ns - это имя пространства имен, а префикс предшествует открывающей скобке { отображения. Кроме того, #:: может использоваться для авторазрешения пространств имен с той же семантикой, что и авторазрешение ключевых слов.

Литерал отображения с синтаксисом пространства имен читается со следующими отличиями от отображений без синтаксиса:

Например, следующий литерал отображения с синтаксисом пространства имен:

#:person{:first "Han"
         :last "Solo"
         :ship #:ship{:name "Millennium Falcon"
                      :model "YT-1300f light freighter"}}

читается как:

{:person/first "Han"
 :person/last "Соло"
 :person/ship {:ship/name "Millennium Falcon"
               :ship/model "YT-1300f light freighter"}}

Множества

Множества - это ноль или более форм, заключенных в фигурные скобки, которым предшествует #: #{:a :b :c}.

Вызовы deftype, defrecord (версии 1.3 и более поздние):

Макросимволы

Поведение парсера определяется комбинацией встроенных конструкций и системой расширения, называемой таблицей чтения. Записи в таблице чтения обеспечивают сопоставление определенных символов, называемых макросимволами, с определенным поведением парсера, называемым макросами парсера. Если не указано иное, макросимволы не могут быть использованы в пользовательских символах.

Цитата (’)

'form(quote form)

Символ (\)

Как указано выше, выдает символьный литерал. Примерами символьных литералов являются: \a \b \c.

Следующие специальные символьные литералы могут быть использованы для обычных символов: \newline, \space, \tab, \formfeed, \backspace и \return.

Поддержка юникода следует соглашениям Java, причем поддержка соответствует версии Java. Литерал юникода имеет форму \uNNNNNN, например, \u03A9 - это литерал для Ω.

Комментарий (;)

Однострочный комментарий, заставляет парсер игнорировать все от точки с запятой до конца строки.

Deref (@)

@form ⇒ (deref form).

Метаданные (^)

Метаданные - это отображение, связанная с некоторыми видами объектов: символы, списки, вектор, множества, отображения, помеченные литералы, возвращающие IMeta, а также вызовы записей, типов и конструкторов. Макрос чтения метаданных сначала считывает метаданные и присоединяет их к следующей прочитанной форме (см. with-meta для присоединения метаданных к объекту):
^{:a 1 :b 2} [1 2 3] дает вектор [1 2 3] с отображением метаданных {:a 1 :b 2}.

Сокращенная версия позволяет метаданным быть простым символом или строкой, в этом случае они рассматриваются как отображение с одним входом с ключом :tag и значением (разрешенного) символа или строки, например:
^String x - то же самое, что ^{:tag java.lang.String} x.

Такие теги можно использовать для передачи информации о типе компилятору.

Другой сокращенный вариант позволяет метаданным быть ключевым словом, в этом случае они рассматриваются как отображение с одним входом с ключом в виде ключевого слова и значением true, например:
^:dynamic x - то же самое, что ^{:dynamic true} x.

Метаданные могут быть соединены в цепочку, в этом случае они объединяются справа налево.

Dispatch (#)

Макрос dispatch заставляет парсер использовать макрос из другой таблицы, индексированной следующим символом

Синтаксическая цитата (`, примечание, символ “обратной цитаты”), снятие цитаты (~) и разделение цитат (~@).

Для всех форм, кроме символов, списков, векторов, множеств и отображений, `x - это то же самое, что и ’x.

Для символов синтаксическая кавычка находит символ в текущем контексте, давая полностью квалифицированный символ (т.е. namespace/name или fully.qualified.Classname). Если символ не соответствует пространству имен и заканчивается на ‘#’, он эквивалентен сгенерированному символу с тем же именем, к которому добавляется ‘_’ и уникальный идентификатор. Например, x# разрешается в x\_123. Все ссылки на этот символ в выражении, заключенном в синтаксические кавычки, разрешаются в тот же генерируемый символ.

Для списков/векторов/множеств/отображений синтаксическая кавычка устанавливает шаблон соответствующей структуры данных. Внутри шаблона неквалифицированные формы ведут себя как рекурсивно цитируемые синтаксические выражения, но формы можно освободить от такого рекурсивного цитирования, квалифицировав их с помощью unquote или unquote-splicing, в этом случае они будут рассматриваться как выражения и заменяться в шаблоне своим значением или последовательностью значений, соответственно.

Например:

user=> (def x 5)
user=> (def lst '(a b c))
user=> `(fred x ~x lst ~@lst 7 8 :nine)
(user/fred user/x 5 user/lst a b c 7 8 :nine)

Таблица чтения в настоящее время недоступна для пользовательских программ.

EDN

Парсер Clojure поддерживает супермножество extensible data notation (edn). Спецификация EDN находится в стадии активной разработки и дополняет этот документ, определяя подмножество синтаксиса данных Clojure нейтральным для языка способом.

Помеченные литералы

Помеченные литералы - это реализация в Clojure EDN помеченных элементов.

Когда Clojure запускается, он ищет файлы с именами data_readers.clj или data_readers.cljc в classpath. Каждый такой файл должен содержать отображение символов Clojure, например, такую:

{foo/bar my.project.foo/bar
 foo/baz my.project/baz}

Ключ в каждой паре - это тег, который будет распознан Clojure парсером. Значение в паре - это полное имя Var, которое будет вызвано парсером для разбора формы, следующей за тегом. Например, учитывая приведенный выше файл data_readers.clj, Clojure парсер будет разбирать эту форму:

#foo/bar [1 2 3]

путем вызова Var #'my.project.foo/bar на векторе [1 2 3]. Функция чтения данных вызывается на форме ПОСЛЕ того, как она была прочитана парсером как обычная структура данных Clojure.

Теги чтения без квалификаторов пространства имен зарезервированы для Clojure. Теги чтения по умолчанию определены в default-data-readers, но могут быть переопределены в data_readers.clj / data_readers.cljc или путем перепривязки *data-readers*. Если для тега не найдено ни одного считывателя данных, то функция, связанная в *default-data-reader-fn*, будет вызвана с тегом и значением для получения значения. Если *default-data-reader-fn* равно nil (по умолчанию), будет вызвано исключение RuntimeException.

Если предоставлен файл data_readers.cljc, он считывается с той же семантикой, что и любой другой cljc-файл с условиями чтения.

Встроенные маркированные литералы

В Clojure 1.4 были введены тегированные литералы instant и UUID. Константы имеют формат #inst "yyyy-mm-ddThh:mm:ss.fff+hh:mm". ПРИМЕЧАНИЕ: Некоторые элементы этого формата являются необязательными. Подробности см. в коде. По умолчанию считывающее устройство будет разбирать предоставленную строку в java.util.Date. Например:

(def instant #inst "2018-03-28T10:48:00.000")
(= java.util.Date (class instant))
;=> true

Поскольку *data-readers* - это динамический var, который можно привязать, вы можете заменить парсер по умолчанию на другой. Например, clojure.instant/read-instant-calendar разберет литерал в java.util.Calendar, а clojure.instant/read-instant-timestamp разберет его в java.util.Timestamp:

(binding [*data-readers* {'inst read-instant-calendar}]
  (= java.util.Calendar (class (read-string (pr-str instant)))))
;=> true

(binding [*data-readers* {'inst read-instant-timestamp}]
  (= java.util.Timestamp (class (read-string (pr-str instant)))))
;=> true

Помеченный литерал #uuid будет разобран в java.util.UUID:

(= java.util.UUID (class (read-string "#uuid \"3b8a31ed-fd89-4f1b-a00f-42e3d60cf5ce\")))
;=> true

Функция чтения данных по умолчанию

Если при чтении помеченного литерала не найдено ни одного устройства чтения данных, вызывается функция *default-data-reader-fn*. Вы можете задать свою собственную функцию чтения данных по умолчанию, а предоставленная функция tagged-literal может быть использована для создания объекта, который может хранить необработанный литерал. Объект, возвращаемый tagged-literal, поддерживает поиск по ключевым словам :tag и :form:

(set! *default-data-reader-fn* tagged-literal)

;; читаем #object как общий объект TaggedLiteral
(def x #object[clojure.lang.Namespace 0x23bff419 "user"])

[(:tag x) (:form x)]
;=> [object [clojure.lang.Namespace 599782425 "user"]]

Reader Conditionals

В Clojure 1.7 появилось новое расширение (.cljc) для переносимых файлов, которые могут быть загружены несколькими платформами Clojure. Основным механизмом управления специфичным для платформы кодом является изоляция этого кода в минимальном наборе пространств имен, а затем предоставление специфичных для платформы версий (.clj/.class или .cljs) этих пространств имен.

В случаях, когда изолировать различные части кода не представляется возможным, или когда код в основном переносимый и имеет только небольшие части, специфичные для платформы, в 1.7 также введены условия чтения, которые поддерживаются только в файлах cljc и в стандартном REPL. Условия чтения следует использовать редко и только в случае необходимости.

Условия чтения - это новая форма диспетчеризации чтения, начинающаяся с #? или #?@. Обе формы состоят из серии чередующихся функций и выражений, аналогично cond. Каждая платформа Clojure имеет известную “особенность платформы” - :clj, :cljs, :cljr. Каждое условие в читающем условии проверяется по порядку, пока не будет найдена функция, соответствующая характеристике платформы. Условие чтения считывает и возвращает выражение этого признака. Выражение в каждой невыбранной ветви будет прочитано, но пропущено. Известная функция :default всегда будет соответствовать и может быть использована для задания значения по умолчанию. Если ни одна ветвь не совпадает, форма не будет прочитана (как если бы не было условного выражения для чтения).

Разработчики неофициальных платформ Clojure должны использовать квалифицированное ключевое слово для своей функции платформы, чтобы избежать коллизии имен. Неквалифицированные свойства платформы зарезервированы для официальных платформ.

Следующий пример будет читаться как Double/NaN в Clojure, js/NaN в ClojureScript и nil в любой другой платформе:

#?(:clj Double/NaN
   :cljs js/NaN
   :default nil)

Синтаксис для #?@ точно такой же, но ожидается, что выражение вернет коллекцию, которая может быть сращена с окружающим контекстом, аналогично сращиванию без кавычек в синтаксисе quote. Использование условного сращивания на верхнем уровне не поддерживается и вызовет исключение. Пример:

[1 2 #?@(:clj [3 4] :cljs [5 6])]
;; в clj => [1 2 3 4]
;; в cljs => [1 2 5 6]
;; в остальных случаях => [1 2]

Функции read и read-string опционально принимают отображение опций в качестве первого аргумента. Текущий набор функций и условное поведение парсера могут быть заданы в отображении опций с помощью этих ключей и значений:

:read-cond - :allow для обработки условий чтения, или
             :preserve - сохранять все ветви
:features - постоянный набор ключевых слов функций, которые активны.

Пример того, как проверить условия чтения ClojureScript из Clojure:

(read-string
  {:read-cond :allow
   :features #{:cljs}}
  "#?(:cljs :works! :default :boo)")
;; :works!

Однако, обратите внимание, что Clojure парсер всегда будет внедрять платформенную функцию :clj. Для чтения, не зависящего от платформы, смотрите tools.reader.

Если парсер вызывается с {:read-cond :preserve}, то условное чтение и неисполненные ветви будут сохранены, как данные, в возвращаемой форме. Условие чтения будет возвращен как тип, поддерживающий поиск ключевых слов для ключей с :form и флагом :splicing?. Прочитанные, но пропущенные помеченные литералы будут возвращены в виде типа, поддерживающего поиск ключевых слов для ключей с ключами :form и :tag.

(read-string
  {:read-cond :preserve}
  "[1 2 #?@(:clj [3 4] :cljs [5 6])]")
;; [1 2 #?@(:clj [3 4] :cljs [5 6])]".

Следующие функции также могут быть использованы в качестве предикатов или конструкторов для этих типов:
reader-conditional? reader-conditional tagged-literal? tagged-literal