Вопрос

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?

Это было полезно?

Решение

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.

Другие советы

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).

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top