Reputation: 12028
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:
String
with a *
operator that works like Python: (* "!" 3)
⇒ "!!!"
, but obscures core's *
just as in my example(ns user (:refer-clojure :exclude [*]))
avoids the "overwriting" warning, but also avoids having that function around :(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
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 defn
s:
(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:
clojure.core/*
for regular old numbersFeel 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