Search code examples
clojurenamespacesprotocols

clojure keeping protocol definition in a separate namespace from implementation


I've been trying to build a discipline of separating my protocol definitions into their own namespace, primarily as a stylistic choice. One thing I don't like about this approach is that since anything a namespace requires is effectively "private" to that namespace, users wanting to call protocol functions from another namespace would have to add a require statement to their code for the protocols.

For example:

Protocol definition namespace:

(ns project.protocols)

(defprotocol Greet
  (greet [this greeting]))

Implementation namespace:

(ns project.entities
 (:require [project.protocols :as protocols]))

(defrecord TheDude
  [name drink]
  protocols/Greet
  (greet [this greeting]
    (println "The Dude sips a" drink)
    (println greeting)))

Core namespace:

(ns project.core
 (:require [project.protocols :as protocols]
           [project.entities :refer [TheDude]]))

(let [dude (TheDude. "Jeff" "white russian")]
  (protocols/greet dude "not on the rug, man..."))

This works just fine, but I don't particularly like that users need to be aware of the need to require project.protocols which is really an implementation detail internal to project.entities. In other languages I would just refer to project.entities/greet within project.core but namespaces don't "export" their required vars in Clojure, they are internal to the requiring namespace only. I see two obvious alternatives, a third could be using something like Potemkin:

  1. Don't put the protocol definitions in a separate namespace, just define them in the same file as the implementation (e.g. project.entities here).
  2. Inside the implementing file, create vars pointing to each and every protocol function (this is very ugly and just feels wrong, but works).

As an example of number 2:

(ns project.entities
 (:require [project.protocols :as protocols]))

(defrecord TheDude 
  [name drink]
  protocols/Greet
  (greet [this greeting]
    (println "not on the rug, man...")
    (println "guess i'll have another " drink)))

(def greet protocols/greet) ; ¯\_(ツ)_/¯

My question, which I suppose is primarily one of preference, is what is the "best practices" (if any) way to handle this sort of separation of concerns? I realize that adding the require in project.core is only one more line, but my concern is less about line count and more about minimizing what a user would need to be aware of.


EDIT: I think the obvious way to accomplish this is to not expect users to require both namespaces, but to create a core namespace which does that for them:

(ns project.core
  (:require [project.protocols :as protocols]
            [project.entities :refer [TheDude]]))

;; create wrapper 'constructor' functions like this for each record in `project.entities`
(defn new-dude 
  [{:keys [name drink] :as dude}]
  (map->TheDude dude))

;; similarly, wrap each protocol method 
(defn greet [person phrase]
  (protocols/greet person phrase))

Now any user can just require core, and if they want to extend the protocol to their own record in a different namespace, they can do so and calls to core/greet will pick up the new implementations. Additionally, if there is any pre/post processing to be done, that can be handled in the "higher level" API function core/greet.


Solution

  • In a program that needs protocols, usually objects are instantiated in some places and consumed (by protocol) in other places. Perhaps in some cases where that's not so, you didn't really need a protocol anyway. In effect, is not too common to "require" the namespaces of both a protocol and an implementation of it. If it happens a lot, it is a code smell.

    pete23's answer mentions using the dot syntax to call a record's method without involving the protocol's namespace. But using the protocol function has some modest advantages.

    Absent implementation inheritance, a protocol contains essential ("primitive") functions only. Such functions are convenient to implement, but not necessarily super-friendly to callers. The protocol namespace is a great place to add non-primitive accessors, of the sort that you might object-orientedly have declared as a default method on an interface or as an inherited non-abstract method on an abstract base class. Consumers that use the protocol's namespace can call the primitives and non-primitives alike.

    Sometimes a primitive turns out to need pre- or post-processing common to all implementations. No need to repeat the common stuff in every implementation! Just lightly refactor: rename the protocol function from f to -f, update the implementations, and add a function f in the protocol's namespace that wraps -f with the necessary pre and post. The callers do not need any change.