Question

I have a protocol called IExample and I define a record type A that implements it:

(defprotocol IExample
  (foo [this] "do something")
  (bar [this] "do something else"))

(defrecord A [field1 field2]
  IExample
  (foo [this]
    (+ field1 field2))
  (bar [this]
    (- field1 field2)))

Let's say I want to extend another (basic) type B to implement this protocol, but I know how to convert from B to A:

 (defn B-to-A
   "converts a B object to an A object"
   [Bobj] ...)

because I have this conversion, I can delegate all calls of the IExample protocol on a B to the IExample protocol on an A by delegating them:

(extend B
  IExample {
    :foo (fn [this] (foo (B-to-A this)))
    :bar (fn [this] (bar (B-to-A this)))})

This, however, seems as an awful lot of boilerplate (especially for bigger protocols) that is not clojure-idiomatic.

How can I tell clojure just to implicitly convert B to A every time an IExample function is called on a B object, using the B-to-A function?

Was it helpful?

Solution

As far as the boilerplate is concerned, you can write some macro to write all that boilerplate for you. On the other hand, you could have a second look at your design here.

What we have here is 3 things (types): A, B and IExample. And then we have 2 relationships between these things: 1) a-to-example : A -> IExample 2) b-to-a : B -> A and from this we can get 3rd relationship by using composition i.e compose b-to-a with a-to-example : B -> IExample. Now if we try to move this design to protocols we will find that it is not a simple translation because protocols won't directly compose as discussed in the above design, instead we can use an intermediate protocol IToExample like shown below:

(defprotocol IExample
  (foo [this] "do something")
  (bar [this] "do something else"))

(defprotocol IToExample
  (to-example [this] "convert to IExample"))

(defrecord A [field1 field2]
  IExample
  (foo [this]
    (+ field1 field2))
  (bar [this]
    (- field1 field2))
  IToExample
  (to-example [this] this))

(deftype B [])
(defn b-to-a [b] (A. ....))
(extend B
  IToExample {:to-example b-to-a})

What we did that we represented the -> IExample in our design as the IToExample protocol with one function. So we got:

  • a-to-example : A -> IExample by implementing IToExample for A
  • b-to-a : B -> A by a normal function
  • compose b-to-a with a-to-example : B -> IExample by implementing IToExample for B and using b-to-a.

OTHER TIPS

It depends. If you look at the clojure core seq functions, you may notice that the ISec interface is only 4 methods, and that the whole "public" sequence library is defined by (many more) functions that call (some-internal-function (seq argument)) - and they tend to be explicitly documented as doing that too. Conceptually, there's an protocol like your IExample interface, and an additional protocol that describes the seq function to convert from some type to something implementing ISeq.

This is an especially useful strategy if the datatype only needs to implement a couple of methods (so IExample can be small) and the number of algorithms acting on the protocol is large (since you can write all of those in terms of regular functions).

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top