Clojure написан в терминах абстракций. Существуют абстракции для последовательностей, коллекций, вызываемости и т.д. Кроме того, Clojure предоставляет множество реализаций этих абстракций. Абстракции задаются интерфейсами хоста, а реализации - классами хоста. Хотя этого было достаточно для начального уровня языка, это оставило Clojure без аналогичных абстракций и низкоуровневых средств реализации. Функции протоколы и типы данных добавляют мощные и гибкие механизмы для абстракции и определения структур данных без компромиссов против средств хостовой платформы.
Существует несколько мотивов для протоколов:
- Предоставить высокопроизводительную, динамическую конструкцию полиморфизма в качестве альтернативы интерфейсам.
- Поддерживать лучшие части интерфейсов
- только спецификация, без реализации
- один тип может реализовать несколько протоколов
- При этом избегая некоторых недостатков:
- Какие интерфейсы будут реализованы - это выбор автора типа во время проектирования, его нельзя расширить позже (хотя внедрение интерфейсов может в конечном итоге решить эту проблему)
- реализация интерфейса создает отношения типа
isa
/instanceof
и иерархию. - избегать “проблемы выражения”, позволяя независимое расширение набора типов, протоколов и реализаций протоколов на типах различными сторонами.
- делать это без оберток/адаптеров
- поддерживать 90% случаев мультиметодов (единая диспетчеризация на тип), обеспечивая при этом абстракцию/организацию более высокого уровня.
Протоколы были введены в Clojure 1.2.
Основное
Протокол - это именованный набор именованных методов и их сигнатур, определяемый с помощью defprotocol:
(defprotocol AProtocol
"A doc string for AProtocol abstraction"
(bar [a b] "Bar docs")
(baz [a] [a b] [a b c] "baz docs"))
- Реализации не предоставляются
- Документация может быть указана для протокола и функций.
- Вышеприведенное дает набор полиморфных функций и объект протокола.
- Все они имеют квалификацию по пространству имен, заключающему определение.
- Результирующие функции выполняют диспетчеризацию по типу своего первого аргумента, и поэтому должны иметь по крайней мере один аргумент
defprotocol
является динамическим и не требует компиляции AOT.
defprotocol автоматически генерирует соответствующий интерфейс с тем же именем, что и протокол, например, если дан протокол my.ns/Protocol, то интерфейс my.ns.Protocol
. Интерфейс будет иметь методы, соответствующие функциям протокола, и протокол будет автоматически работать с экземплярами интерфейса.
Обратите внимание, что вам не нужно использовать этот интерфейс с deftype, defrecord, или reify, так как они поддерживают протоколы напрямую:
(defprotocol P
(foo [x])
(bar-me [x] [x y]))
(deftype Foo [a b c]
P
(foo [x] a)
(bar-me [x] b)
(bar-me [x y] (+ c y)))
(bar-me (Foo. 1 2 3) 42)
= > 45
(foo
(let [x 42]
(reify P
(foo [this] 17)
(bar-me [this] x)
(bar-me [this y] x))))
> 17
Клиент Java, желающий участвовать в протоколе, может сделать это наиболее эффективно, реализуя интерфейс, созданный протоколом.
Внешние реализации протокола (которые необходимы, когда вы хотите, чтобы класс или тип, не находящийся под вашим контролем, участвовал в протоколе) могут быть предоставлены с помощью конструкции extend:
(extend AType
AProtocol
{:foo an-existing-fn
:bar (fn [a b] ...)
:baz (fn ([a]...) ([a b] ...)...)}
BProtocol
{...}
...)
extend
принимает тип/класс (или интерфейс, см. ниже), одну или несколько пар протокол + пар названий и определений функций.
- Расширяет полиморфизм методов протокола для вызова предоставленных функций, если в качестве первого аргумента указан AType.
- Пары названий и определений функций - это пары из имен методов с ключевыми словами как названиями и обычными функциями.
- это облегчает повторное использование существующих функций и пар, для повторного использования кода/миксинов без деривации или композиции.
- Вы можете реализовать протокол на интерфейсе.
- это в первую очередь облегчает взаимодействие с хостом (например, Java)
- но открывает дверь к случайному множественному наследованию реализации.
- поскольку класс может наследоваться от более чем одного интерфейса, оба из которых реализуют протокол
- если один интерфейс является производным от другого, то используется более производный, иначе какой из них используется, не уточняется.
- Реализующая функция может предполагать, что первый аргумент является инстансом (
instanceof
)AType
. - Вы можете реализовать протокол на
nil
. - Чтобы определить реализацию протокола по умолчанию (не для nil), просто используйте
Object
.
Протоколы полностью ретифицированы и поддерживают возможности рефлексии через extends? , extenders и satisfies? .
- Обратите внимание на удобные макросы extend-type и extend-protocol.
- Если вы предоставляете внешние определения в строке, они будут более удобны, чем прямое использование
extend
.
(extend-type MyType
Countable
(cnt [c] ...)
Foo
(bar [x y] ...)
(baz ([x] ...) ([x y zs] ...)))
; раскрывается в
(extend MyType
Countable
{:cnt (fn [c] ...)}
Foo
{:baz (fn ([x] ...) ([x y zs] ...))
:bar (fn [x y] ...)})
Рекомендации по расширению
Протоколы - это открытая система, расширяемая до любого типа. Чтобы минимизировать конфликты, примите во внимание следующие рекомендации:
- Если вы не являетесь владельцем протокола или целевого типа, вы должны расширять его только в коде приложения (не в публичных библиотеках) и ожидать, что он может быть нарушен одним из владельцев.
- Если вы владеете протоколом, вы можете предоставить некоторые базовые версии для общих целей как часть пакета.
- Если вы поставляете библиотеку потенциальных целей, вы можете предоставить реализации общих протоколов для них, с учетом того, что вы диктуете. Вам следует проявлять особую осторожность при расширении протоколов, включенных в сам Clojure.
- Если вы являетесь разработчиком библиотеки, вы не должны расширять, если вы не являетесь владельцем ни протокола, ни типа.
Также смотрите это обсуждение в списке рассылки.
Расширение через метаданные
Начиная с версии Clojure 1.10, протоколы могут быть расширены с помощью метаданных по каждому значению:
(defprotocol Component
:extend-via-metadata true
(start [component]))
Когда :extend-via-metadata
равен true
, значения могут расширять протоколы, добавляя метаданные, где ключи - это полностью квалифицированные символы функций протокола, а значения - реализации функций. Реализации протоколов проверяются сначала на прямые определения (defrecord
, defype
, reify
), затем на определения метаданных, затем на внешние расширения (extend
, extend-type
, extend-protocol
).
(def component (with-meta {:name "db"} {`start (constantly "started")}))
(start component)
;;=> "started"