Search code examples
clojureprotocolsimplicit-conversioncoalescing

clojure: can I define an implicit conversion possility?


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?


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.