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

Мотивация

Clojure написан в терминах абстракций. Существуют абстракции для последовательностей, коллекций, вызываемости и т.д. Кроме того, Clojure предоставляет множество реализаций этих абстракций. Абстракции задаются интерфейсами хост-платформы, а реализации - классами хост-платформы. Хотя этого было достаточно для начального уровня языка, это оставило Clojure без аналогичных абстракций и низкоуровневых средств реализации. Функции протоколы и типы данных добавляют мощные и гибкие механизмы для абстракции и определения структур данных без компромиссов против средств хост-платформы.

Основное

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

deftype и defrecord

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

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

Зачем нужны и deftype, и defrecord?

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

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

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

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

Datatypes and protocols навязывают вам некоторые выборы

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

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

reify

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

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

Результат - лучшая производительность по сравнению с прокси, как при построении, так и при вызове. reify предпочтительнее прокси во всех случаях, когда его ограничения не являются запретительными.

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

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

Пример:

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

(definterface Foo (foo []))

;; аннотация по типу
(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
       ;; на поле
       ^{: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]
  ;; на методе
  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)))