Clojure

协议

动机

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。该接口将具有与协议函数相对应的函数,并且协议将自动与接口的实例一起使用。

请注意,您不需要将此接口与 deftypedefrecordreify 一起使用,因为它们直接支持协议

(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 作为第一个参数时调用提供的函数

  • 函数映射是将关键字化的函数名称映射到普通 fn 的映射

    • 这便于轻松重用现有的 fn 和映射,用于代码重用/混合,无需派生或组合

  • 您可以在接口上实现协议

    • 这主要是为了便于与主机(例如 Java)进行互操作

    • 但也打开了实现的偶然多重继承的大门

      • 因为一个类可以继承自多个接口,而这两个接口都实现了该协议

      • 如果一个接口是从另一个接口派生的,则使用派生的更多接口,否则使用哪个接口是不确定的。

  • 实现的 fn 可以假定第一个参数是 AType 的实例

  • 您可以在 nil 上实现协议

  • 要定义协议的默认实现(针对除 nil 之外的其他情况),只需使用 Object

协议是完全具象化的,并通过 extends?extenderssatisfies? 支持反射功能。

  • 请注意便捷宏 extend-typeextend-protocol

  • 如果您要提供内联的外部定义,与直接使用 extend 相比,这些将更加方便

(extend-type MyType
  Countable
    (cnt [c] ...)
  Foo
    (bar [x y] ...)
    (baz ([x] ...) ([x y zs] ...)))

  ;expands into:

(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、deftype、reify),然后是元数据定义,然后是外部扩展(extend、extend-type、extend-protocol)。

(def component (with-meta {:name "db"} {`start (constantly "started")}))
(start component)
;;=> "started"