Clojure

Структуры данных

Clojure имеет богатый набор структур данных, которые объединяют следующие свойства:

  • Они неизменяемы

  • Они предназначены для чтения

  • Они правльно поддерживают метод equals

  • Они предоставляют качественные hash-значения

  • Также, коллекции:

    • Управляются через интерфейсы.

    • Поддерживают итерирование

    • Поддерживают сохраняемые манипуляции.

    • Поддерживают метаданные

    • Реализуют java.lang.Iterable

    • Реализуют обязательные методы (для чтения) класса java.util.Collection

nil

nil - это возможное значение любого типа данных в Clojure. nil имеет то же самое значение, что и null в Java. Система проверки условий Clojure основана на nil и false, которые представляют логическую ложь, когда любое другое значение представляет логическую истину. Также, nil используется как маркер конца последовательности в протоколе последовательностей.

Числа

Clojure по-умолчанию предоставляет полную поддержку значений-примитивов JVM, что позволяет реализовывать высокопроизводительный код для вычислительных приложений.

Также Clojure поддерживает классы-обертки Java, наследованные от java.lang.Number, включая BigInteger и BigDecimal, плюс свой собственный класс Ratio. Существуют следующие особенности:

Long

По-умолчанию Clojure оперирует натуральными числами как экземплярами примитивного типа long. Когда примитивные операции возвращают значение, которое не вмещается в long генерируется java.lang.ArithmeticException. Clojure предоставляет множество альтернативных операторов, заканчивающися апострофом: +', -', *', inc', и dec'. Эти операторы автоматически преобразуют число в BigInt при переполнении, но работают медленнее, чем обычные операторы.

Ratio

Представляет отношение между целыми числами. Результат деления целых чисел, который сам не является целым числом порождает дробь, т.е. 22/7 = 22/7, а не число с плавающей точкой и не число, обрезанное до целого.

Заражение

Объекты BigInt и типы чисел с плавающей точкой "заражают драгих" при операциях. Например, любые целочисленные операции, включающие BigInt будут в результате давать BigInt, а любые операции включающие double или float будут в результате давать double.

Литералы BigInt и BigDecimal

Целочисленные литералы для BigInt и BigDecimal определяются при помощи суффиксов N и M соответственно.

Выражение

Возвращаемое значение

(== 1 1.0 1M)

true

(/ 2 3)

2/3

(/ 2.0 3)

0.6666666666666666

(map #(Math/abs %) (range -3 3))

(3 2 1 0 1 2)

(class 36786883868216818816N)

clojure.lang.BigInt

(class 3.14159265358M)

clojure.lang.BigDecimal

Функции

Вычисление: + - * / inc dec quot rem min max
Автоматически расширяющие вычисления: +' -' *' inc' dec'
Сравнение: == < > >= zero? pos? neg?
Битовые операции: bit-and bit-or bit-xor bit-not bit-shift-right bit-shift-left
Соотношения: numerator denominator
Приведение: int bigdec bigint double float long num short

Строки

Строки в Clojure являются объектами-строками Java. См. также Печать.

user=> (map (fn [x] (.toUpperCase x)) (.split "Dasher Dancer Prancer" " "))
("DASHER" "DANCER" "PRANCER")

Символы

Символы в Clojure такие же как в Java.

Ключевые слова

Ключевые слова - символические идентификаторы, которые разрешаются сами в себя. Они позволяют значительно быстрее производить проверку на равенство. Как символы, они имеют имена и необязательное пространство имен, оба из которых являются строками. Начальный ':' не является частью пространства имен или имени.

Ключевые слова реализуют интерфейс IFn, чтобы вызывать invoke() с одним аргументом (ассоциативным массивом) и необязательным вторым аргументом (значение по-умолчанию). Например (:mykey my-hash-map :none) означает тоже самое, что (get my-hash-map :mykey :none). См. get.

Функции

Символы

Символы - это идентификаторы, которые обычно используются, чтобы на что-то ссылаться. Они моугт быть использованы в формах программы, чтобы ссылаться на параметры функции, устанавливать связи, имена классов и глобальные переменные. У них есть имена и необязательное пространство имён, оба - строки. Символы могут иметь метаданные (см. with-meta).

Символы, как и ключевые слова, реализуют интерфейс IFn, чтобы вызывать invoke() с одним аргументом(ассоциативным массивом) и необязательным вторым аргументом (значение по-умолчанию). Например ('mysym my-hash-map :none) означает то же самое что (get my-hash-map 'mysym :none). См. get.

Функции

symbol symbol? gensym (см. также макрос считывателя с суффиксом \#)

Коллекции

Все коллекции Clojure являются неизменяемыми и стойкие. В частности, коллекции Clojure поддерживают эффективное создание 'модифицированных' версий, с использованием структурного обмена. Коллекции эффективны и потоконезависимы. Коллекции представлены в виде абстракций, и у них может быть одна или несколько конекретных реализаций. В частности, так как операции 'модификации' порождают новую коллекцию, эта новая коллекция может не иметь тот же самый тип, что исходная, но будет реализовывать тот же логический тип (интерфейс).

Все коллекции поддерживают count чтобы узнать размер коллекции, conj для 'добавления' элементов и seq чтобы получать последовательность, которая поможет пройти по всей коллекции, хотя их конкретное поведение немного отличается для различных типов коллекций.

Так как коллекции поддерживают функцию seq, все функции обработки последовательностей могут быть использованы с любой коллекцией.

Хеш-суммы коллекций Java

Интерфейсы коллекций Java определяют алгоритмы вычисления hashCode() для классов List, Set, and Map. Все алгоритмы вычисления hashCode() коллекций Clojure соответствуют этим определениям.

Хеш-суммы коллекций Clojure

Clojure предоставляет свой алгоритм вычисления хеш-суммы, немного лучше работающий с коллекциями и другими типами, известный как hasheq.

Интерфейс IHashEq помечает коллекции, которые предоставляют функцию hasheq() - для вычисления значения hasheq. Также, в Clojure функция hash может быть использована, чтобы вычислить значение hasheq.

Упорядоченные коллекции (векторы, списки, последовательности и т.д.) обязаны использовать следующий алгоритм для вычисления haseq (где hash вычисляет hasheq). Заметим, что unchecked-add-int и unchecked-multiply-int используются для выполнения операций над целыми числами с переполнением.

(defn hash-ordered [collection]
  (-> (reduce (fn [acc e] (unchecked-add-int
                            (unchecked-multiply-int 31 acc)
                            (hash e)))
              1
              collection)
      (mix-collection-hash (count collection))))

Неупорядоченные коллекции (ассоциативные массивы, множества) обязаны использовать следующий алгоритм для вычисления hasheq. Элемент ассоциативного массива рассматривается как упорядоченная коллекция, состоящая из ключа и значения. Заметим, что unchecked-add-int используется для выполнения операций над целыми числами с переполнением.

(defn hash-unordered [collection]
  (-> (reduce unchecked-add-int 0 (map hash collection))
      (mix-collection-hash (count collection))))

Алгоритм работы mix-collection-hash - это деталь реализации и может изменяться.

Списки (IPersistentList)

Списки являются коллекциями. Они реализуют интерфейс ISeq напрямую (кроме пустых списков, которые не являются корректной последовательностью). Функция count имеет сложность O(1). Функция conj добавляет элементы в начало списка.

Функции

Создать список: list list*
Работать со списком как со стеком: peek pop
Проверить, является ли объект списком: list?

Вектора (IPersistentVector)

Вектор - это пронумерованная коллекция значений. Вектора поддерживают доступ к элементу по его номеру за log32N шагов. count - за время O(1). conj помещает элемент в конец вектора. Вектора также поддеживают rseq, которая возвращает элементы в обратном порядке. Вектора реализуют IFn, для invoke() с одним аргументом, который рассматривается как номер. В качестве результата возвращается элемент, соответствующий этому номеру. Таким образом, вектора - это функции, переводящая номер в элемент. Вектора сначала сравниваются по длине, потом поочередно сравниваются элементы.

Функции

Создать вектор: vector vec vector-of
Проверить вектор: get nth peek rseq vector?
'Изменить' вектор: assoc pop subvec replace

Также смотри zipper-ы

Ассоциативный массив (IPersistentMap)

Ассоциативный массив - это коллекция, которая ставит в соответствие ключи и значения. Предоставляются два типа ассоциативных массивов - сортированные и хэшированные. Хэшированные массивы требуют, чтобы ключи поддерживали hashCode и equals. Сортированные массивы требуют, чтобы ключи реализовывали Comparable, или экземпляр класса Comparator. Хэшированные массивы предоставляют более быстрый доступ log32N шагов против logN шагов, но сортированные массивы зато отсортированы. count - сложность O(1). conj ожидает другой ассоциативный массив (возможно состоящий из одной записи), как аргумент, и возвращает новый массив, содержащий записи из старого и из нового, причем новые записи перезаписывают старые. conj также принимают MapEntry или вектор из двух элементов (ключ и значение). seq возвращает последовательность записей массива, т.е. пары ключ-значение. Сортированные массивы также поддерживают rseq, которая возвращает записи в обратном порядке. Ассоциативные массивы реализуют IFn, для invoke() с одним аргументом (ключем) с опциональным вторым аргументом (значение по-умолчанию), то есть ассоциативные массивы - это функции их ключей. Значечния ключей и значений nil разрешены.

Функции

Создать новый ассоциативный массив: hash-map sorted-map sorted-map-by
'Изменить' массив: assoc dissoc select-keys merge merge-with zipmap
Просмотреть ассоциативный массив: get contains? find keys vals map?
Просмотреть элемент массива: key val

StructMap-ы

В большинстве случаев, вместо использования StructMap лучше использовать records.

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

Чтобы создать StructMap нужно сначала создать базовый объект структуры с помощью create-struct или defstruct, затем создать экземпляры с помощью struct-map или struct.

(defstruct desilu :fred :ricky)
(def x (map (fn [n]
              (struct-map desilu
                :fred n
                :ricky 2
                :lucy 3
                :ethel 4))
             (range 100000)))
(def fred (accessor desilu :fred))
(reduce (fn [n y] (+ n (:fred y))) 0 x)
 -> 4999950000
(reduce (fn [n y] (+ n (fred y))) 0 x)
 -> 4999950000

Функции

Объявление StructMap: create-struct defstruct accessor
Создать отдельную структуру: struct-map struct

ArrayMap-ы

При выполнении манипуляций с кодом часто хочется иметь ассоциативный массив поддерживающий упорядоченные ключи. ArrayMap это как раз такой ассоциативный массив - он просто реализован как массив пар ключ-значение. Скорость поиска в нем возрастает по линейному закону и он подходит только для очень маленьких ассоциативных массивов. Он реализует интерфейс ассоциативных массивов целиком. Новый ArrayMap может быть создан с помощью функции array-map. Также ArrayMap будет поддерживать порядок пока не 'модифицирован'. Последующее изменение заставит его 'превратиться' в хэшированный ассоциативный массив.

Множества

Множества - это коллекции уникальных значений.

Существует поддержка множества с помощью литералов:

#{:a :b :c :d}
-> #{:d :a :b :c}

Вы можете создать множество с помощью функций hash-set и sorted-set:

(hash-set :a :b :c :d)
-> #{:d :a :b :c}

(sorted-set :a :b :c :d)
-> #{:a :b :c :d}

Вы также можете получить множество значений коллеции с помощью функции set:

(set [1 2 3 2 1 2 3])
-> #{1 2 3}

Множества - это коллекции:

(def s #{:a :b :c :d})
(conj s :e)
-> #{:d :a :b :e :c}

(count s)
-> 4

(seq s)
-> (:d :a :b :c)

(= (conj s :e) #{:a :b :c :d :e})
-> true

Множества поддерживают 'удаление' с помощью disj, а также contains? и get, последний возвращает объект, который содержится внутри множества и равен ключу, если таковой имеется:

(disj s :d)
-> #{:a :b :c}

(contains? s :b)
-> true

(get s :a)
-> :a

Множества - это функции своих членов, что реализовано с помощью get:

(s :b)
-> :b

(s :k)
-> nil

Clojure предоставляет базовый набор операция типа union / difference / intersection, также как поддержку некоторый псевдо-реляционной алгебры для 'отношений', которые являются просто множетсвом ассоциативных массивов - select / index / rename / join.