Clojure является гомоиконическим языком, что является причудливым термином, описывающим тот факт, что программы на Clojure представлены структурами данных Clojure. Это очень важное различие между Clojure (и Common Lisp) и большинством других языков программирования - Clojure определяется в терминах оценки структур данных, а не в терминах синтаксиса потоков символов/файлов. Программы на Clojure зачастую преобразовывают и создают другие программы на Clojure.
Тем не менее, большинство программ Clojure начинают свою жизнь как текстовые файлы, и задача парсера - разобрать текст и создать структуру данных, которую увидит компилятор. Это не просто этап работы компилятора. Парсер и представления данных Clojure в стандартном выводе сами по себе полезны во многих тех же контекстах, в которых можно использовать XML, JSON и т.п.
Можно сказать, что синтаксис парсера определяется в терминах символов, а синтаксис языка Clojure - в терминах символов, списков, векторов, хэшей и т.д. Парсер представлен функцией read, которая читает следующую форму (не символ) из потока и возвращает объект, представленный этой формой.
Поскольку мы должны с чего-то начинать, эта ссылка начинается там, где начинается оценка, с форм парсера. Это неизбежно повлечет за собой разговор о структурах данных, описание которых и интерпретация компилятором будут следующими.
Формы парсера
Символы
- Символы начинаются с нечислового символа и могут содержать буквенно-цифровые символы и *, +, !, -, _, ’, ?, <, > и = (со временем могут быть разрешены и другие символы).
- ‘/’ имеет особое значение, его можно использовать один раз в середине символа для отделения пространства имен от имени, например,
my-namespace/foo
. ‘/’ сам по себе называет функцию деления. - ‘.’ имеет особое значение - он может использоваться один или несколько раз в середине символа для обозначения полностью определенного имени класса, например,
java.util.BitSet
, или в именах пространств имен. Символы, начинающиеся или заканчивающиеся на ‘.’, зарезервированы в Clojure. Символы, содержащие / или . считаются содержащими пространство имен. - Символы, начинающиеся или заканчивающиеся на ‘:’, зарезервированы в Clojure. Символ может содержать один или несколько неповторяющихся символов ‘:’.
Литералы
- Строки - Заключаются в “двойные кавычки”. Могут разбиты на несколько линий. Поддерживаются стандартные символы экранирования Java.
- Числа - обычно представляются в соответствии с Java.
- Целочисленные числа могут быть неопределенно длинными и будут читаться как
Long
, если находятся в диапазоне, и какclojure.lang.BigInt
в противном случае. Целочисленные числа с суффиксомN
всегда читаются какBigInt
. Восьмеричная нотация допускается с префиксом0
, а шестнадцатеричная - с префиксом0x
. Когда возможно, они могут быть указаны в любом основании от 2 до 36 (см.Long.parseLong()
); например,2r101010
,052
,8r52
,0x2a
,36r16
и42
- это все одно и то жеLong
. - Числа с плавающей точкой читаются как числа с двойной точностью; с суффиксом
M
они читаются как большие десятичные числа. - Поддерживаются рациональные числа, например,
22/7
- Символы - перед ними ставится обратная косая черта:
\c
.\newline
,\space
,\tab
,\formfeed
,\backspace
и\return
дают соответствующие символы. Символы юникода представляются с помощью\uNNNNN
, как в Java. Окталы представляются с помощью\oNNN
. nil
означает “ничто/не-значение” - представляетnull
в Java и проверяет логическую ложь.- Булевы значения -
true
иfalse
- Символьные значения -
##Inf
,##-Inf
и##NaN
. - Ключевые слова подобны символам, за исключением:
- Они могут и должны начинаться с двоеточия, например, :fred.
- Они не могут содержать ‘.’ в части имени, или классов имен.
- Как и символы, они могут содержать пространство имен, `:person/name’, которое может содержать символы ‘.’.
- Ключевое слово, начинающееся с двух двоеточий, автоматически преобразуется в текущем пространстве имен в ключевое слово, включающее простаранство имен:
- Если ключевое слово не включает простаранство имен, то пространством имен будеттекущим. В
user
,::rect
читается как:user/rect
. - Если ключевое слово включает простаранство имен, пространство имен будет разрешено с использованием псевдонимов в текущем пространстве имен. В пространстве имен, где
x
является псевдонимомexample
,::x/foo
разрешается как:example/foo
.
- Если ключевое слово не включает простаранство имен, то пространством имен будеттекущим. В
Списки
Списки - это ноль или более форм, заключенных в круглые скобки: (a b c)
.
Векторы
Векторы - это ноль или более форм, заключенных в квадратные скобки: [1 2 3]
.
Отображения
- Отображения - это ноль или более пар ключ/значение, заключенных в фигурные скобки:
{:a 1 :b 2}
. - Запятые считаются пробелами и могут быть использованы для упорядочивания пар:
{:a 1, :b 2}
. - Ключи и значения могут быть любой формы.
Синтаксис пространства имен отображения
Добавлен в 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 и более поздние):
- Вызовы конструкторов Java class, deftype и defrecord могут быть вызваны с использованием полного имени класса, которому предшествует # и за которым следует вектор:
#my.klass_or_type_or_record[:a :b :c]
. - Элементы векторной части передаются без вычисления в соответствующий конструктор. Экземпляры
defrecord
также могут быть созданы с помощью аналогичной формы, которая принимает отображение:#my.record{:a 1, :b 2}
. - Ключевые значения в отображении присваиваются без вычисления соответствующим полям в defrecord. Любым полям записи без соответствующих записей в литеральной отображении присваивается значение nil. Любые дополнительные ключевые значения в литерале отображений добавляются к результирующему экземпляру
defrecord
.
Макросимволы
Поведение парсера определяется комбинацией встроенных конструкций и системой расширения, называемой таблицей чтения. Записи в таблице чтения обеспечивают сопоставление определенных символов, называемых макросимволами, с определенным поведением парсера, называемым макросами парсера. Если не указано иное, макросимволы не могут быть использованы в пользовательских символах.
Цитата (’)
'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 заставляет парсер использовать макрос из другой таблицы, индексированной следующим символом
{} - см. множества
- Regex-шаблоны (#“pattern”)
- Var-quote (#’)
- Литерал анонимной функции (#())
- Игнорировать следующую форму (#_)
Синтаксическая цитата (`, примечание, символ “обратной цитаты”), снятие цитаты (~) и разделение цитат (~@).
Для всех форм, кроме символов, списков, векторов, множеств и отображений, `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