Simple Clojure Protocols Tutorial

Published Jan 23, 2017
Simple Clojure Protocols Tutorial

Protocols Overview

  • Provide a high-performance, dynamic polymorphism construct as an alternative to interfaces
  • Specification only, no implementation
  • Protocols are a mechanism for polymorphism.
  • Protocols can be useful for defining an external boundary, such as an interface to a service. In this case, it's useful to have polymorphism so we can substitute different services, or use mock services for testing.
  • A protocol is a set of methods. The protocol has a name and an optional documentation string. Each method has a name, one or more argument vectors, and an optional documentation string. That's it! There are no implementations, no actual code.
  • An important difference between protocols and interfaces: protocols have no inheritance. You cannot create “subprotocols” like Java's subinterfaces.
  • A datatype is not required to provide implementations for every method of its protocols or interfaces. Methods lacking an implementation will throw an AbstractMethodError when
    called on instances of that data type.

Extending Protocols to Already Existing Types

  • To create a new protocol that operates on an existing datatype and f.ex when you cannot modify the source code
    of the defrecord. You can still extend the protocol to support that datatype, using the extend function:
(extend DatatypeName
     SomeProtocol
        {:method-one (fn [x y] ...)
         :method-two existing-function}
     AnotherProtocol
        {...})

Extend takes a datatype name followed by any number of protocol/method map pairs. A method
map is an ordinary map from method names, given as keywords, to their implementations.

  • Use extend-type when you want to implement several protocols for the same datatype; use extend-protocol when you want to implement the same protocol for several datatypes.

How protocols solve the expression problem

Expression problem

The basic problem of extensibility: our programs manipulate data types using operations.

As our programs evolve, we need to extend them with new data types and new operations. Particularly, we want to be able to add new operations that work with the existing data types and new data types that work with the existing operations. Furthermore, we want this to be a true extension (i.e. we don't want to modify the existing program), we want to respect the existing abstractions, and we want our extensions to be in separate modules and namespaces. We also want the extensions to be separately compiled, deployed, and type checked.

  • Multimethods already help solve the expression problem; the main thing Protocols offer over Multimethods is Grouping: you can group multiple functions together and say "these 3 functions together form Protocol Foo". You cannot do that with Multimethods — they always stand on their own.

  • Why protocols when we have multimethods?

Any platform that you would like Clojure to run on (JVM, CLI, ECMAScript, Objective-C) has specialized high-performance support for dispatching solely on the type of the first argument. Clojure Multimethods OTOH dispatch on arbitrary properties of all arguments.

So, Protocols restrict you to dispatch only on the first argument and only on its type (or as a special case on nil).

OO Style with Protocols Often Hides Obvious & Simple Things

(defprotocol Saving
  (save [this] "saves to mongodb")
  (collection-name [this] "must return a string representing the associated MongoDB collection"))

;Default implementation

(extend-type Object
  Saving
  ; the `save` method is common for all, so it is actually implemened here
  (save [this] (mc/insert (collection-name [this]) this))
  ; this method is custom to every other type
  (collection-name [this] "no_collection"))

;Particular implementations

(defrecord User
  [login password]
  Saving
  (collection-name [this] "users"))

(defrecord NewsItem
  [text date]
  Saving
  (collection-name [this] "news_items"))

Instead of this, where save fn won't work on User and NewsItem, use:

Make the save function a normal function:

(defn save [obj] (mc/insert (collection-name obj) obj))
The protocol should only have collection-name

(defprotocol Saving
  (collection-name [this] "must return a string representing the associated MongoDB collection"))

Each object that wants to be "saved" can implement this protocol.

Discover and read more posts from Mouna Cheikhna
get started
Enjoy this post?

Leave a like and comment for Mouna

1