Clojure

Типы данных: deftype, defrecord и reify

Мотивация

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

Основы

Функции объявления типов deftype , defrecord и reify позволяют объявлять реализации абстракций и в случае reify - экземпляры этих реализаций. Абстракции сами по себе определяются либо с помощью протоколов, либо - интерфейсов. Тип данных генерирует соответствующий ему Java-класс (именованный в случае deftype и defrecord, анонимный в случае reify), с некоторой структурой (явные поля в случае deftype и defrecord, неявные замыкания в случае reify), и необязательные внутренние реализации абстрактных методов. Они поддерживают доступ к высокопроизводительным примитивным представлениям и полиморфизму Java. Обратите внимание, что они не прото обертка над Java. Они поддерживают только ограниченное множество возможностей Java, часто с большим динамизмом, чем сама Java. Идея в том, что если interop не заставляет выйти за пределы их ограниченного объема, не нужно оставлять Clojure, чтобы получить на платформе самые высокопроизводительные структуры данных.

deftype and defrecord

deftype и defrecord динамически генерируют скомпилированный байткод именованного класса с заданным набором полей и, что необязтельно, методами для оного или более протоколов и/или интерфейсов. Они подходят для динамического и интерактивного программирования, не требуют компиляции перед исполнением и могет быть переопределены в рамках одной сессии. Они похожи на defstruct тем, что генерируют структуры данных с именованными полями, но отличаются от defstruct в следующем:

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

  • полученный класс имеет правильный тип, в отличие от соглашений по кодированию типа для структур в метаданных

  • так как они генерируют именованный класс, он имеет доступный конструктор

  • поля могут иметь подсказки о типах и могут быть примитивами

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

    • планируется ограничение для типов полей и агрументов конструктора

  • deftype/defrecord могут реализовывать один или более протоколов и/или интерфейсов

  • deftype/defrecord могут быть запсаны специальным синтексисом считавателя #my.thing[1 2 3] где:

    • каждый элемент в вектор-форме передается в конструктор deftype/defrecord невычисленным

    • им deftype/defrecord должно быть уточненным

    • это доступно только в Clojure версии 1.3 и выше

  • когда deftype/defrecord Foo определено соответствующая функция ->Foo определена и передает свои аргументы в конструктор Foo (версии 1.3 и выше)

deftype и defrecord отличаются в следующем:

  • deftype не предоставляет никакой функциональности, не добавленной пользователем, кроме конструктора

  • defrecord предоставляет полную реализацию ассоциативного массива, в том числе:

    • equal и hashCode основанную на значениях

    • поддержку метаданных

    • поддержку ассоциаций

    • доступ к полям по ключемым словам

    • расширяемые поля (вы можете добвлять значения, ассоциированные с ключами, которых не было изначально)

    • и т.д.

  • deftype поддерживает изменяемые поля, defrecord - нет

  • defrecord поддерживает дополнительную форму считывателя #my.record{:a 1, :b 2} принимающую ассоциативный массив, который инициализирует defrecord:

    • имя defrecord должно быть уточнено

    • элементы в массиве невычислены

    • существующие поля defrecord принимают указанные в массиве значения

    • поля defrecord, которых нет в массиве устанавливаются в nil

    • дополнительные пары ключ-значение разрешены и добавляются в defrecord

    • доступно только в Clojure версии 1.3 и выше

  • если определена defrecord Bar, то соответствующая функция map->Bar определена тоже и она принимает ассоциативный массив, который использует для инициализации нового экземпляра Bar (только 1.3 и выше)

Почему существуют сразу и deftype и defrecord?

Оказывается, что классы в большинстве объектно-ориентированных программах принадлежат одной из двух категорий: классы, являющиеся артефактами области реализации/программирования, например String или классы коллекций или ссылочные типы Clojure; и классы, которые представляют информацию о области приложения, например Employee, PurchaseOrder и т.д. Всегда была печальная практика использования классов для информации домена приложения, которая привела к тому, что информация была скрыта за специфичными для класса микроязыками, например даже кажующийся безобидным employee.getName() - это настраиваемый интерфейс для данных. Хранение информации в таких классах является проблемой, как если бы каждая книга была бы написана на своем языке. Вы больше не можете использовать общий подход к обработке информации. Это приводит к возрастанию ненужной специфичности и недостатку повторного использования.

Вот почему Clojure всегда поощряет хранение такой информации в ассоциативных массивах и этот совет касается и типов данных. Используя defrecord вы получаете единый способ доступа к информации, плюс дополнительные преимущества полиморфизма типов и структурной эффективности полей. С другой стороны, нет смысла для типа данных, который определяет коллекцию вроде вектора иметь реализацию ассоциативного массива, и в этом случае как раз лучшим образом подходит deftype.

В целом, defrecord будут лучше чем structmap во всех инормационно-ориентированных случаях и вы должны заменить все structmap на defrecord. Вряд ли многие пытались использовать structmap для программирования конструкций, но если это так - вы найдете deftype более подходящими.

Компилируемые перед исполнением deftype/defrecord могут подходить как замена gen-class, когда не мешают их ограничения. В этом случае они будут иметь лучшую производительность, чем gen-class.

Типы данных и протоколы догматичны

Хотя типы данных и протоколы имеют хорошо определенные отношения с конструкциями Java и сделаны для того, чтобы предоставить наилучшую функциональность Clojure для программ Java, они не являются в первую очередь конструкциями общения с Java. То есть они не прикладывают никаких усилий, чтобы полностью имитировать или адаптировать объектно-ориентированные механизмы Java. В частности, они отражают следующее:

  • Конкретное наследование - плохо

    • вы не можете наследовать типы данных от конкретных классов, только от интерфейсов

  • Вы всегда должны программировать протоколы или интерфейсы

    • типы данных не могут предоставлять методы, которых нет в их протоколах или интерфейсах

  • Неизменяемость должна быть по-умолчанию

    • и быть единственным вариантом для defrecord

  • Инкапсуляция информации - глупость

    • поля - публичны, используйте протоколы/интерфейсы чтобы избежать зависимостей

  • Связывание полиморфизма с наследованием - плохо

    • протоколы свободны от этого

Если вы используете типы данных и протоколы - вы сможете предложить пользователям чистый, основанный на интерфейсах API. Если вы имеете дело с чистым, основанным на интерфейсах Java API, типы данных и протоколы могут быть использованы, чтобы взаимодействовать и расширять его. Если у вас имеется 'плохой' Java API, вы будете должны использовать gen-class. Только таким образом программные конструкции, которые вы используете чтобы разрабатывать и реализовывать программы на Clojure, будут свободны от непредвиденных сложностей объектно-ориентированного программирования.

reify

В то время, как deftype и defrecord определяют именованные типы, reify определяет и анонимный тип и создает экземпляр этого типа. Это помогает, когда вам необходима единовременная реализация одного или более протоколов или интерфейсов и вы бы хотели воспользоваться локальным контекстом. В этом отношении такой вариант использования равносилен proxy или анонимным внутренним классам в Java.

Тела методов reify - это лексические замыкания, и могут ссылаться на окружающий локальный контекст. reify отличается от proxy в следующем:

  • Поддерживаются только протоколы или интерфейсы, не конкретные классы-родители.

  • Тела методов - истинные методы класса-результата, не внешние функции.

  • Исполнение методов экземпляра происходит напрямую, не через поиск в ассоциативном массиве.

  • Нет поддержки динамического обмена методов в массиве методов.

В результате получается лучшая производтельность, чем у proxy, и при создании и при исполнении. reify более предпочтителен, чем proxy во всех случаях, где это не запрещено.

Поддержка аннотаций Java

Типы, созданные с помощью deftype, defrecord, и definterface, могут производить классы, содержащие аннотации для интеграции с Java. Аннотации описываются как метаданные на:

  • Именах типов (deftype/record/interface) - аннотации классов

  • Именах полей (deftype/record) - аннотации полей

  • Именах методов (deftype/record) - аннотации методов

Пример:

(import [java.lang.annotation Retention RetentionPolicy Target ElementType]
        [javax.xml.ws WebServiceRef WebServiceRefs])

(definterface Foo (foo []))

;annotation on type
(deftype ^{Deprecated true
            Retention RetentionPolicy/RUNTIME
            javax.annotation.processing.SupportedOptions ["foo" "bar" "baz"]
            javax.xml.ws.soap.Addressing {:enabled false :required true}
            WebServiceRefs [(WebServiceRef {:name "fred" :type String})
                            (WebServiceRef {:name "ethel" :mappedName "lucy"})]}
  Bar [^int a
       ;on field
       ^{:tag int
          Deprecated true
          Retention RetentionPolicy/RUNTIME
          javax.annotation.processing.SupportedOptions ["foo" "bar" "baz"]
          javax.xml.ws.soap.Addressing {:enabled false :required true}
          WebServiceRefs [(WebServiceRef {:name "fred" :type String})
                          (WebServiceRef {:name "ethel" :mappedName "lucy"})]}
       b]
  ;on method
  Foo (^{Deprecated true
          Retention RetentionPolicy/RUNTIME
          javax.annotation.processing.SupportedOptions ["foo" "bar" "baz"]
          javax.xml.ws.soap.Addressing {:enabled false :required true}
          WebServiceRefs [(WebServiceRef {:name "fred" :type String})
                          (WebServiceRef {:name "ethel" :mappedName "lucy"})]}
       foo [this] 42))

(seq (.getAnnotations Bar))
(seq (.getAnnotations (.getField Bar "b")))
(seq (.getAnnotations (.getMethod Bar "foo" nil)))