Протоколы

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

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

Протоколы были введены в 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 автоматически генерирует соответствующий интерфейс с тем же именем, что и протокол, например, если дан протокол 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 принимает тип/класс (или интерфейс, см. ниже), одну или несколько пар протокол + пар названий и определений функций.

Протоколы полностью ретифицированы и поддерживают возможности рефлексии через extends? , extenders и satisfies? .

(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 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"