Fork me on GitHub
#clojure
<
2024-01-30
>
p-himik14:01:10

I feel like there's been some recent discussion about it but couldn't find it. Just to confirm - there are no ways, existing or planned, to easily decorate a protocol method on its implementation, right? Apart from using things like https://github.com/deckchair-technicians/bowen. So e.g. if type T implements methods a, b, c on protocol P and I want to wrap only the implementation of a with my own code, I currently have to specify all those methods in reify or my own type that delegates b and c to the wrapped object. An example from next.jdbc: https://github.com/seancorfield/next-jdbc/blob/develop/src/next/jdbc/result_set.clj#L184-L200

chrisn15:01:36

I have a version of protocols that allow partial overriding with fallbacks to the parent classes - https://github.com/cnuernber/clojure/blob/faster-map/src/clj/clojure/core_deftype.clj#L600 The gist is to have the methodimplcache called from the method, and not go through the impl table. They are also faster for extend but that is another discussion. So at some point I will have a version of Clojure that does allow what you are talking about - I would term it partial overriding of methods of a baseclass or something like that.

Ed17:01:04

I don't know if this is poking into the implementation details in a way that's not to be recommended, but you can call (:impls AProtocol) and get a map of the current implementations of that protocol, then use merge and extend to update the impls? As in

(defprotocol TestFish
    (one [this x])
    (two [this x]))

  (extend java.lang.String
    TestFish
    {:one (fn [this x] (str this :one x))
     :two (fn [this x] (str this :two x))})

  (extend-type java.lang.Long
    TestFish
    (one [this x] (+ this 1 x))
    (two [this x] (+ this 2 x)))

  (one "test" "test") ;; => "test:onetest"
  (two "test" "test") ;; => "test:twotest"
  (one 1 2)           ;; => 4
  (two 1 2)           ;; => 5

  (extend java.lang.String
    TestFish
    (merge (-> TestFish :impls (get java.lang.String))
           {:one (fn [this x] (str this :other :one x))}))

  (extend java.lang.Long 
   TestFish
    (update (-> TestFish :impls (get java.lang.Long))
           :two (fn [f] (fn [this x] (* 2 (f this x))))))

  (one "test" "test") ;; => "test:other:onetest"
  (two "test" "test") ;; => "test:twotest"
  (one 1 2)           ;; => 4
  (two 1 2)           ;; => 10
I'm sure it breaks in all kinds of cases, in horrible ways, but does it help?

p-himik17:01:17

That replaces an existing implementation. Whereas I need to duplicate an implementation for my own type and override/wrap one method. It does sound possible with extend and :impls, but no clue whether there are any consequences w.r.t. e.g. tagging or something like that. And I'm certainly less interested in poking around the internals and more interested in writing code that I can safely forget about for a decade. :)

Ed17:01:36

I think that extend is totally supported - the bit I'm unsure about is calling :impls on the protocol - but that seems to be how extend works. I would have thought it would be easy enough to get all the functions for one type and merge in your own function for your new type. But yeah, totally see your point 😉

frozenlock21:01:34

@U2FRKM4TW Do you have total control over the protocol? You could set :extend-via-metadata and provide the wrapped methods in an object metadata.

p-himik21:01:26

I do not, no.

frozenlock21:01:22

But I see exactly what you mean; even with metadata middleware I found myself wishing for something similar a few times.

frozenlock21:01:12

"I need this things, buuuuuut I want to slightly change this method on-the-fly. All the other methods need to remain the same and be available."