Fork me on GitHub
#clojure
<
2023-07-04
>
frozenlock00:07:57

I have an object implementing a protocol and I'd like to modify/wrap some of those methods. The object also has other methods which are unrelated. Is there a way to return (a copy of?) the object but with a few methods modified? Something like (update-method object :some-method my-wrapper).

Bob B01:07:25

Are you saying you want to change the method on one instance of a type? If so, then I don't think so, because at that point the protocol would have to dispatch on reference, not type.

frozenlock02:07:16

Pretty much. I can easily use a map and provide the wrapped methods as metadata, but unfortunately I lose access to the other methods.

Max04:07:37

Why not make a new implementation of the protocol that defers to the original object for the unmodified methods? You’d have to implement all the methods on this proxy impl but they wouldn’t do much

didibus05:07:19

Can't you use reify? It lets you override methods and tskes an object, but the state is lost, it'll return a new object with methods overriden

frozenlock11:07:37

In both of those, I will lose the method I don't manually reify, won't I? Say for example that there's a .close method, but I only reify method1 and method2, wouldn't this break with-open?

frozenlock11:07:45

I don't know in advance what methods will be on the object, only that it will at least implement a particular protocol.

Max14:07:57

Where does this object come from? Is it code you control?

frozenlock14:07:08

No, all I can guarantee is that it will implement a particular protocol. It might implement many others, or have java methods like .close.

Max14:07:16

I’m not aware of any way to do what you’re describing. Any way I know to implement a protocol requires you to specify all of the protocols you’re implementing and results in a new object.

Max14:07:47

Though perhaps it’d be possible to build those protocol and method definitions via reflection?

Max14:07:58

I suppose another approach would be to define a new protocol for your modified behavior, extend it to the object’s type, and only call your new methods from within your code

frozenlock14:07:47

I want to modify a single instance, not the whole type. I guess I'm doing something wrong in my design; there doesn't seem to be a way to do it in Clojure. 😕

frozenlock14:07:43

Reflection is interesting... Is it considered reliable? All I know is that I saw an option "warn on reflection" in a few projects.

Max14:07:15

Yes, it’s a Java feature, it’s not generally recommended because it’s relatively slow and fragile. I would probably be somewhat suspicious of seeing reflection used outside of “appropriate use cases” (automatically generating mocks, debugging tools, other dev-time stuff) but I’ve seen worse hacks in production code. Reflection warnings are something different. They mean that you didn’t use a type hint somewhere you should’ve and so Clojure is using (slower) reflection to make your code work. In general though I would recommend reconsidering the design. Anything you build with reflection will be a hack, albeit potentially a useful one. But if the problem is self-inflicted, simply not having the problem is almost certainly better

frozenlock15:07:00

Alright, thank you very much for your help!

Mark Wardle16:07:18

I assume you mean interface not protocol? This isn't a limitation of Clojure, but a limitation of the JVM. You can use reflection and a dynamic proxy to create a facade for any arbitrary object on the JVM, but the Clojure style is to think in terms of protocols and interfaces, so I'd create a facade type, or reify, in order to re-implement the interface required, likely calling back to the original object and modifying the results as necessary. Java has dynamic proxies to satisfy a more dynamic facade that essentially tunnels all calls to a single method which you then dispatch to, which could then dispatch to the underlying object unless you have chosen to re-implement.

frozenlock17:07:23

> so I'd create a facade type, or reify, in order to re-implement the interface required, likely calling back to the original object and modifying the results as necessary The issue I have is that by doing this I lose access to all the other methods on this object. The ones I don't manually reify and can't know in advance that they exist.

frozenlock17:07:08

I'm really looking for a map-like behavior: (assoc my-map :a "hello") will return a map with "hello" associated at the key :a. I don't know what other entries were already in the map, but they remain available for anyone downstream.

didibus17:07:52

Hum, reify wouldn't lose the other methods, it will just overwrite them. Though now I'm not sure about the protocol extended methods.

didibus17:07:27

But what method are you trying to overwrite? Java methods? Or Protocol methods?

frozenlock17:07:06

I want to augment some protocol methods, in the same way as we can add middleware in web handlers.

frozenlock17:07:03

As far as I know, reify doesn't take an object as an argument. For example, I can't modify the toString method of a map doing this: (reify {} Object (toString [] "abcd"))

Mark Wardle17:07:41

The dynamic proxy option might be your best bet then? I’ve not used it, either from Java or Clojure, but should do what you need to create a facade that delegates except for the ones you choose not to.

didibus17:07:24

And your target object is a Java object? Or some clojure data which supports metadata?

didibus17:07:59

If the latter, I think you can use extend via metadata

didibus17:07:08

Extend via metadata As of Clojure 1.10, protocols can optionally elect to be extended via per-value metadata: (defprotocol Component :extend-via-metadata true (start [component])) When :extend-via-metadata is true, values can extend protocols by adding metadata where keys are fully-qualified protocol function symbols and values are function implementations. Protocol implementations are checked first for direct definitions (defrecord, deftype, reify), then metadata definitions, then external extensions (extend, extend-type, extend-protocol). (def component (with-meta {:name "db"} {`start (constantly "started")})) (start component) ;;=> "started"

frozenlock17:07:38

Metadata extension is what I'm currently using, but it has the issue I'm describing. Not all objects support metadata and metadata has lower precedence, so I must wrap the object into something else for metadata to work. Then if a consumer tries to call .close (or any other method out of my control) it will throw an exception.

didibus17:07:41

Hum. Ok. Well two things. First, reify should work to override, like say override toString. But it's not extending existing types, it creates a new type with the method overridden. Second, it seems you don't control the Objects, and they could be a mixture of Java Objects and Clojure data. But you do control the protocol? If so, you can switch to Multi-methods instead. Then you can dispatch on whatever you want, even the memory location of the object.

didibus18:07:20

You could also shenanigan the extended functions themselves.

frozenlock18:07:07

I don't really see how multimethods could work in this case :thinking_face: To keep with your example, let's say we have start. I don't know what is the implementation of start for the current object, but I'd like to call start-backup-server whenever start is called. (-> my-webserver wrap-backup-server --start--) Those wrapper can be in any order... (-> my-webserver wrap-email-status wrap-backup-server ...) Or do you mean that the start implementation itself should call a multimethod?

didibus18:07:14

(defprotocol Foo
  (bar [this x y]))
(defn foo-impl
  [this x y]
  (if-let [f (get @foo-impls (System/identityHashCode this))]
    (f x y)
    ... ; Default impl goes here)
(defn override
  [obj impls-atom f]
  (swap! impls-atom assoc (System/identityHashCode obj) f))
Something like that. I wrote this on my phone, so it would probably need massaging. But also, identityHashCode is not guaranteed to perfectly be unique for each instance. It uses hashCode method, which defaults to something mostly unique to the instance.

didibus18:07:11

With Multi-method, ya start would be a Multi-method. And the dispatch function would do something like, check if the object has a wrapped start, if so, call that. Else call the one of the type of the object.

didibus19:07:40

The issue with any external extension like that though, is some object you just can't uniquely identify. Even identityHashCode can have collisions. I think it just generates a random int, so it's not using UUID or anything like that, so collisions are more likely.

didibus19:07:58

Objects that have metadata will carry the functions on them, so that works fine.

Max19:07:43

Frankly, if you control all of the locations where start is called, I would just write a new start fn that calls both that object’s start and start-backup-server and not futz with protocols at all, then only call your new start in all those places where the old start was called before. If you want it to be extensible a la middlewares, you could also design some sort of registry/wrapping thing, but I wouldn’t base it off of protocols and I’d still make a new thing rather than trying to augment an existing thing

💯 2
didibus19:07:44

Oh, I mixed up reify and proxy before...

didibus19:07:10

Ya, I think you'll need to reconsider your approach.

Pratik12:07:29

Can we redef a state? I am defining a datasource like this

(defstate datasource
  :start (do (log/info "Starting DB connection pool")
             (hikari/make-datasource (:db config/config)))
  :stop (do (hikari/close-datasource datasource)
            (log/info "Closed DB connection pool")))
I have another datasource which is using replica instead of master and that’s using different port than the main datasource. Whenever I run test in my local environment or via the CI pipeline, we don’t have this master-replica set up and it can’t connect to the 5433 port. For running the tests, I want to redef the new read-only datasource state with the older state. Is that possible? I am getting below error trying to do that with with-redefs:
class clojure.core$constantly$fn__5672 cannot be cast to class javax.sql.DataSource (clojure.core$constantly$fn__5672 is in unnamed module of loader 'app'; javax.sql.DataSource is in module java.sql of loader 'platform') 

pithyless13:07:14

I'm guessing this is mount's defstate? It's not a regular def (it's a macro with other machinery). Have you seen if https://github.com/tolitius/mount#swapping-alternate-implementations does what you need for swapping states?

Pratik13:07:11

Yes, it’s mount’s defstate , I checked the doc around swapping implementations but I am not getting how can I swap it with existing datasource

Pratik13:07:55

I have

(defstate datasource
  :start (do (log/info "Starting DB connection pool")
             (hikari/make-datasource (:db config/config)))
  :stop (do (hikari/close-datasource datasource)
            (log/info "Closed DB connection pool")))

(defstate read-only-datasource
  :start (do (log/info "Starting read-only DB connection pool")
             (hikari/make-datasource (merge (:db config/config)
                                            (:read-only-db config/config))))
  :stop (do (hikari/close-datasource datasource)
            (log/info "Closed read-only DB connection pool")))
and I was trying something like
(mount/swap {#'db/read-only-datasource db/datasource})
but that’s not working

Pratik13:07:25

I don’t want to create another datasource by using mount/start-with-states

pithyless13:07:26

Caveat: I am not familiar with mount. Why not treat read-write and read-only datasource as two separate configs (e.g. in a regular def) and then just start/stop the one datasource with a specific config (depending on context)?

Pratik13:07:54

I need both the datasources, read-only is just for an API where we want to return historical data and read-write is for all the other APIs

pithyless13:07:00

so, if I understand correctly defstate datasource will always use primary-config, but defstate read-only-datasource will be started with read-only-config or primary-config (depending on context)

Pratik13:07:16

sorry, I guess this is confusing:

(merge (:db config/config)
                                            (:read-only-db config/config)))
it was done because apart from port and connection pool size all the other configs were same, so I would just overwrite those 2 configs and keep rest the same

Pratik13:07:14

but I need both the datasources, read-only for historical API and read-write for all the other ones

valerauko15:07:40

What I'd do is extract the :start bit as a function and pass in the differing bit as a parameter If you insist on using the same defstate for this purpose, maybe have some environment-specific way to load config? Eg a env/dev/app/config.clj and env/prd/app/config.clj which define the differing bits of configuration?

Pratik16:07:21

but I need two defstates as I mentioned, so how would parameterising help here?

cjmurphy17:07:35

There is a #C0H7M5HFE channel.

gratitude-thank-you 2
Caio Cascaes17:07:08

Do we have any good tutorials or how-to guides on using state-flow? After a six-month hiatus, I'm finding it difficult to remember how to use it properly.

Caio Cascaes17:07:47

Some examples may help

phronmophobic19:07:17

Is state-flow a library?

phronmophobic18:07:19

neat! I hadn't heard of that library.