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)
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. Когда возможно, они могут быть заданы в любой системе счисления от 2 до 36 (см. Long.parseLong()); например 2r101010, 8r52, 36r16 и 42 - все являются Long.
Числа с плавающей запятой прочитываются как Double; с суффиксом M - как BigDecimal.
Поддерживаются дроби, например: 22/7.
Символы - начинаются с обратной косой черты: \c. \newline, \space, \tab, \formfeed, \backspace и \return производят соответствующие символы. Символы Юникода записываются \uNNNN, как в Java. Окталы записываются \oNNN.
nil означает "ничего", "нет значения" - соответствует null в Java и логическому false.
Логические - true и false.
Ключевые слова - как знаки, за изключением:
Они могут и должны начинаться с ':', например :fred.
Они не могут содержать '.' или имена классов.
Как и знаки, они могут содержать пространство имен: :person/name
Ключевое слово, начинающееся с двух двоеточий соответствует текущему пространству имен:
В пространстве имен user, '::rect' прочитывается как :user/rect
Списки - ноль или более форм, заключенных в скобки: (a b c)
Вектора - ноль или более форм, заключенных в квадратные скобки: [1 2 3]
Ассоциативный массив - ноль или более пар ключ-значение, заключенных в фигурные скобки: {:a 1 :b 2}
Запятые рассматриваются как пробельный символ и могут быть использованы для огранизации пар: {:a 1, :b 2}
В качестве ключей и значений могут выступать любые формы.
Множества - ноль или более форм, заключенных в фигурные скобки и начинающиеся с #
: #{:a :b :c}
Вызовы конструкторов Java классов, deftype и defrecord могут быть выполнены с использованием полного имени класса с # перед ним и вектором после: #my.klass_or_type_or_record[:a :b :c]
Элементы в векторе передаются "невычисленными" в соответствующий конструктор. Экземпляры defrecord также могут быть созданы похожим образом, с использованием ассоциативного массива вместо вектора: #my.record{:a 1, :b 2}
Значения, ассоциированные с ключами, передаются "невычисленными" в соответствующие поля defrecord. Всем полям defrecord, которым не соответствует пара ключ-значение, будет присвоено значение nil. Дополнительные пары ключ-значение будут добавлены в конструируемый экземпляр defrecord.
Поведение считывателя управляется с помощью комбинации встроеных конструкций и системы расширений, называемой таблицей чтения. Ячейки таблицы чтения содержат соответствия между особенными символами, называемыми макросимволами, и конкретными действиями при их чтении, называемыми макросами. Если не заявлено обратное, макросимволы не могут быть использованы внутри пользовательских знаков.
'form
⇒ (quote form)
Как указано выше, производят символьный литерал. Примеры символьных литералов: \a \b \c
.
Следующие специальные символьные литералы могут быть использованы в качестве символов: \newline
, \space
, \tab
, \formfeed
, \backspace
, and \return
.
Поддержка Unicode зависит от особенностей поддержки Unicode внутренней версией Java. Литералы Unicode записываются формой \uNNNN
, например \u03A9
- литерал для Ω.
Однострочный комментарий, заставляющий считыватель игнорировать все, начиная с текущей позиции до конца строки.
@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
Метаданные могут быть сцеплены. В этом случае они объединяются как ассоциативные массивы в направлении справа налево.
Управляющий символ заставляет считыватель использовать макрос из другой таблицы, содержащей следующие записи:
#{} - используется для объявления множеств, как было упомянуто выше
Регулярные выражения (#"pattern")
Регулярные выражения прочитываются и компилируются во время чтения. В результате получается экземпляр класса java.util.regex.Pattern. Регулярные выражения не поддерживают те же правила экранирования что и обычные строки, а именно: обратная косая черта в регулярных выражениях обрабатывается сама по себе (и не должна экранироваться с помощью дополнительной черты). Например, (re-pattern "\\s*\\d+")
может быть записано более кратко как #"\s*\d+"
.
Апостроф var (#')
#'x
⇒ (var x)
- возвращает переменную как объект, а нее её значение.
Анонимная функция (#())
#(…)
⇒ (fn [args] (…))
где args определяются по наличию литералов аргументов вида %, %n или %&. % - синоним для %1, %n обозначает n-тый аргумент (отсчет начинается с 1-го) и %& обозначает остальные аргументы. Это не замена для fn - корректно использовать их для очень короткоживующих функций для сопоставления/фильтрации и т.п. Формы #() не могут быть вложенными.
Игнорировать следующую форму (#_)
Форма, следующая за #_ полностью пропускается считывателем. (Это даже более полное удаление чем макрос comment, который порождает nil).
Для всех форм кроме знаков, списков, векторов, множеств и ассоциативных массивов, `x - это же самое что 'x.
Для знаков, цитирование разрешает знак в текущем контексте, порождая уточненный знак (т.е. namespace/name или fully.qualitied.Classname). Если знак не принадлежит пространству имен и заканчивается на '#' он разрешается в сгенерированный знак с тем же именем, но с уникальным id добавленным в конец через '_'. То есть x# будет разрешен в x_123. Все ссылки на этот знак внутри выражения, помеченного цитированием, разрешаются в этот сгенерированный символ.
Для списков, векторов, множеств и ассоциативных массивов цитирование создает соответствующую структуру данных. Внутри нее все формы ведут себя как рекурсивно помеченные цитированием, кроме помеченных подстановкой или подстановкой-сращиванием. В этом случае они будут обработаны как выражения и будут заменены в созданной структуре данных своими значениями или последовательностями значений, соответственно.
Например:
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)
На данный момент таблица чтения недоступна для пользовательских программ.
Считыватель языка Clojure поддерживает часть extensible data notation (edn). Спецификация edn пока еще в разработке, но она дополняет этот документ и определяет часть синтксиса данных Clojure.
Маркированные литералы - это реализация tagged elements на Clojure.
При старте Clojure ищет файлы, называющиеся data_readers.clj
в корне classpath. Каждый такой файл должен содержать ассоциативный массив из знаков, например:
{foo/bar my.project.foo/bar
foo/baz my.project/baz}
Ключем в каждой паре является тег, который распознается считывателем. Значение - обработчик, уточненное имя переменной, которая будет вызвана считывателем как функция для обработки формы, следующую за тегом. Например, для data_readers.clj из примера выше считыватель распознает такую форму:
#foo/bar [1 2 3]
Вызвав #'my.project.foo/bar
на векторе [1 2 3]
. Функция вызывается в на форме ПОСЛЕ того, как она будет прочитана считывателем как любая другая обычная структура данных Clojure.
Теги без уточнения пространства имен зарезервированы Clojure. Теги, зарегистрированные по-умолчанию определены в default-data-readers, но могут быть переопределены в data_readers.clj
или с помощью переопределения *data-readers*. Если тег не зарегистрирован, будет вызвана функция *default-data-reader-fn*, которой этот тег будет передан, чтобы получить нужный обработчик. Если *default-data-reader-fn* возвратит nil (что является поведением по-умолчанию) - будет сгенерировано исключение RuntimeException.
Clojure 1.7 ввела новое расширение (.cljc) для переносимых файлов, которые могут быть загружены несколькими платформама Clojure. Главный механизм для управления кодом, зависящим от платформы, - изолировать этот код в минимальное множество пространств имен, а затем предоставить платформенно-зависимые версии этих пространств имен (.clj/.class или .cljs).
В случаях, когда невозможно изолировать различные части кода или когда код портируется с небольшими изменениями от платформы к платформе, Clojure 1.7 предоставляет ветвления считывателя, которые поддерживаются только в cljc-файлах и в REPL. Ветвления должны использоваться редко и только по необходимости.
Ветвления - это новый макрос управляющего символа, начинающийся с #?
или #?@
. Обе записи должны состоять из нескольких альтернативных характерстик и соответствующих им выражений, так же как и у функции cond
. Каждая платформа Clojure имеет известную "характеристику платформы" - :clj
, :cljs
, :cljr
. Каждое условие в ветвлении считыватель проверяет по порядку, пока не найдет подходящее под характеристику платформы. Тогда считыватель прочитает и возвратит выражение, соответствующее этой характеристике. Остальные выражения будут прочитаны и пропущены. Характеристика :default
будет подходить под любую платформу и может быть использована для значений по-умолчанию. Если подходящих условий не будет найдено, то и форм не будет прочитано (как если бы условного выражения reader-а не было бы совсем).
Следующий пример будет прочитан как Double/NaN в Clojure, js/NaN в ClojureScript и nil на всех других платформах:
#?(:clj Double/NaN
:cljs js/NaN
:default nil)
Синтаксис для #?@
такой же, но ожидается, что выражение возвращает коллекцию, которая может быть вставлена в окружение, также как подстановка-сращивание (см. макрос цитирования выше). Использование такого сращивания на верхнем уровне не поддерживается и будет генерировать исключение. Например:
[1 2 #?@(:clj [3 4] :cljs [5 6])]
;; in clj => [1 2 3 4]
;; in cljs => [1 2 5 6]
;; anywhere else => [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