chbrown
chbrown

Reputation: 12028

Making Clojure's defprotocol play nice (polymorphically) with existing functions

How can I write a defprotocol (and defrecord to implement it) that declares a method with the same name as an existing function, and dispatch dynamically to the protocol/record's method iff I call it with an instance of the protocol/record, but otherwise dispatch to the existing function?

For example, I want to create a geometry helper that supports basic arithmetic (just multiplication in this example, to keep it short):

(defprotocol SizeOps
  (* [this factor] "Multiply each dimension by factor and return a new Size"))

At this point I'm already getting some foreboding pushback from the compiler:

Warning: protocol #'user/SizeOps is overwriting function *
WARNING: * already refers to: #'clojure.core/* in namespace: user, being replaced by: #'user/*

Then the implementation:

(defrecord Size [width height]
  SizeOps
  (* [this factor] (Size. (* width factor) (* height factor))))

That compiles okay, but when I try to use it, the only * it knows is the one in my protocol:

(* (Size. 1 2) 10)

IllegalArgumentException No implementation of method: :* of protocol: #'user/SizeOps found for class: java.lang.Long

I can hack around this by fully specifying the core * function in my implementation:

(defrecord Size [width height]
  SizeOps
  (* [this factor] (Size. (clojure.core/* width factor) (clojure.core/* height factor))))
(* (Size. 1 2) 10)

#user.Size{:width 10, :height 20}

But I get the same IllegalArgumentException if I try to call (* 3 4) later on. I can stomach using the namespaced clojure.core/* in my defrecord implementation, but I want my users to be able to call * on my Size records as well as on Long, Double, etc., as usual.

Similar Q&A:

I suspect the right solution lies somewhere in lower-level dispatch functionality like defmulti and defmethod or deftype / derive but I'm not super familiar with the nuances of Clojure's runtime polymorphism. And I'm gonna have a whole host of Size, Point, Rectangle, Circle, etc., types that each support some subset of +, -, *, / operations, so I'd love to know if there's a way to tell defprotocol to participate in / build on the polymorphism of any existing functions rather than simply overwrite them.

Upvotes: 4

Views: 1034

Answers (1)

Sam Estep
Sam Estep

Reputation: 13294

In cases like this, when you run into limitations of protocols in and of themselves, it can help to create a separate function that simply calls the protocol method for some of its functionality, and does the rest of what needs to be done using the additional capabilities that are given to regular defns:

(ns example.size
  (:refer-clojure :exclude [*])
  (:require [clojure.core :as clj]))

(defprotocol SizeOps
  (times [this factor]))

(extend-protocol SizeOps
  Object
  (times [this factor] (clj/* this factor)))

(defrecord Size [width height]
  SizeOps
  (times [this factor] (->Size (clj/* width factor) (clj/* height factor))))

(defn *
  ([] (clj/*))
  ([x] (clj/* x))
  ([x y] (times x y))
  ([x y & more] (apply clj/* x y more)))

There are a couple advantages to the specific approach I've taken here:

  • All paths except the two-argument path just use arity dispatch (which is fast), and the two-argument path only additionally uses protocol dispatch (which I think is as fast as you're generally going to get for what you're trying to do)
  • You keep all the arities, so the behavior should be identical to clojure.core/* for regular old numbers

Feel free to optimize any of this as needed.

Finally, to demonstrate:

(ns example.core
  (:refer-clojure :exclude [*])
  (:require [example.size :refer [* ->Size]]))

(* (->Size 1 2) 10) ;=> #example.size.Size{:width 10, :height 20}
(* 3 4) ;=> 12

Hopefully sufficiently ergonomic, as alluded to earlier.

Upvotes: 5

Related Questions