Мотивация
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 может быть записан с помощью специального синтаксиса чтения
#my.thing\[1 2 3\]
, где: - каждый элемент в векторной форме передается в конструктор deftype/defrecord без оценки
- имя deftype/defrecord должно быть полностью определено.
- доступно только в версиях Clojure ниже 1.3
- при определении deftype/defrecord Foo определяется соответствующая функция
->Foo
, которая передает свои аргументы в конструктор (только версии 1.3 и более поздние)
deftype и defrecord отличаются следующим образом:
- deftype не предоставляет никакой функциональности, не указанной пользователем, кроме конструктора
- defrecord предоставляет полную реализацию персистентного отображения, включая:
- равенство на основе значений и hashCode
- поддержку метаданных
- ассоциативную поддержку
- доступ к полям с помощью ключевых символов;
- расширяемые поля (вы можете подключать ключи, не поставляемые с определением defrecord)
- и т.д.
- deftype поддерживает изменяемые поля, defrecord - нет.
- defrecord поддерживает дополнительную форму чтения
#my.record{:a 1, :b 2}
, принимая отображение, которое инициализирует defrecord в соответствии с: - имя defrecord должно быть полностью определенным
- элементы в отображении не подлежат оценке
- существующие поля defrecord принимают ключевые значения
- поля записи без ключевых значений в литеральном отображении инициализируются в nil
- дополнительные ключевые значения разрешены и добавляются к записи defrecord
- доступно только в версиях Clojure ниже 1.3
- когда определяется defrecord Bar, определяется соответствующая функция
map->Bar
, которая принимает отображение и инициализирует новый экземпляр записи ее содержимым (только версии 1.3 и более поздние)
Зачем нужны и 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:
- Имя типа (deftype/record/interface) - аннотации класса.
- Имена полей (deftype/record) - аннотации полей.
- Имена методов (deftype/record) - аннотации методов.
Пример:
(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)))