Clojure

Считыватель

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}

deftype, defrecord и вызовы конструкторов (версия 1.3 и выше):

  • Вызовы конструкторов 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 - литерал для Ω.

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

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

Оператор 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

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

Управляющий символ (#)

Управляющий символ заставляет считыватель использовать макрос из другой таблицы, содержащей следующие записи:

  • #{} - используется для объявления множеств, как было упомянуто выше

  • Регулярные выражения (#"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)

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

Расширяемое представление данных (edn - extensible data notation)

Считыватель языка 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