Clojure

Protocols

Motivation

Clojure is written in terms of abstractions. There are abstractions for sequences, collections, callability, etc. In addition, Clojure supplies many implementations of these abstractions. The abstractions are specified by host interfaces, and the implementations by host classes. While this was sufficient for bootstrapping the language, it left Clojure without similar abstraction and low-level implementation facilities. The protocols and datatypes features add powerful and flexible mechanisms for abstraction and data structure definition with no compromises vs the facilities of the host platform.

There are several motivations for protocols:

  • Provide a high-performance, dynamic polymorphism construct as an alternative to interfaces

  • Support the best parts of interfaces

    • specification only, no implementation

    • a single type can implement multiple protocols

  • While avoiding some of the drawbacks

    • Which interfaces are implemented is a design-time choice of the type author, cannot be extended later (although interface injection might eventually address this)

    • implementing an interface creates an isa/instanceof type relationship and hierarchy

  • Avoid the 'expression problem' by allowing independent extension of the set of types, protocols, and implementations of protocols on types, by different parties

    • do so without wrappers/adapters

  • Support the 90% case of multimethods (single dispatch on type) while providing higher-level abstraction/organization

Protocols were introduced in Clojure 1.2.

Basics

A protocol is a named set of named methods and their signatures, defined using defprotocol:

(defprotocol AProtocol
  "A doc string for AProtocol abstraction"
  (bar [a b] "bar docs")
  (baz [a] [a b] [a b c] "baz docs"))
  • No implementations are provided

  • Docs can be specified for the protocol and the functions

  • The above yields a set of polymorphic functions and a protocol object

    • all are namespace-qualified by the namespace enclosing the definition

  • The resulting functions dispatch on the type of their first argument, and thus must have at least one argument

  • defprotocol is dynamic, and does not require AOT compilation

defprotocol will automatically generate a corresponding interface, with the same name as the protocol, e.g. given a protocol my.ns/Protocol, an interface my.ns.Protocol. The interface will have methods corresponding to the protocol functions, and the protocol will automatically work with instances of the interface.

Note that you do not need to use this interface with deftype , defrecord , or reify, as they support protocols directly:

(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

A Java client looking to participate in the protocol can do so most efficiently by implementing the protocol-generated interface.

External implementations of the protocol (which are needed when you want a class or type not in your control to participate in the protocol) can be provided using the extend construct:

(extend AType
  AProtocol
   {:foo an-existing-fn
    :bar (fn [a b] ...)
    :baz (fn ([a]...) ([a b] ...)...)}
  BProtocol
    {...}
...)

extend takes a type/class (or interface, see below), a one or more protocol + function map (evaluated) pairs.

  • Will extend the polymorphism of the protocol’s methods to call the supplied functions when an AType is provided as the first argument

  • Function maps are maps of the keywordized method names to ordinary fns

    • this facilitates easy reuse of existing fns and maps, for code reuse/mixins without derivation or composition

  • You can implement a protocol on an interface

    • this is primarily to facilitate interop with the host (e.g. Java)

    • but opens the door to incidental multiple inheritance of implementation

      • since a class can inherit from more than one interface, both of which implement the protocol

      • if one interface is derived from the other, the more derived is used, else which one is used is unspecified.

  • The implementing fn can presume first argument is instanceof AType

  • You can implement a protocol on nil

  • To define a default implementation of protocol (for other than nil) just use Object

Protocols are fully reified and support reflective capabilities via extends? , extenders , and satisfies? .

  • Note the convenience macros extend-type and extend-protocol

  • If you are providing external definitions inline, these will be more convenient than using extend directly

(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] ...)})

Guidelines for extension

Protocols are an open system, extensible to any type. To minimize conflicts, consider these guidelines:

  • If you don’t own the protocol or the target type, you should only extend in app (not public lib) code, and expect to maybe be broken by either owner.

  • If you own the protocol you get to provide some base versions for common targets as part of the package, subject to the dictatorial nature of doing so.

  • If you are shipping a lib of potential targets you can provide implementations of common protocols for them, subject to the fact that you are dictating. You should take particular care when extending protocols included with Clojure itself.

  • If you are a library developer, you should not extend if you own neither the protocol nor the target

Also see this mailing list discussion.

Extend via metadata

As of Clojure 1.10, protocols can optionally elect to be extended via per-value metadata:

(defprotocol Component
  :extend-via-metadata true
  (start [component]))

When :extend-via-metadata is true, values can extend protocols by adding metadata where keys are fully-qualified protocol function symbols and values are function implementations. Protocol implementations are checked first for direct definitions (defrecord, deftype, reify), then metadata definitions, then external extensions (extend, extend-type, extend-protocol).

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