This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-07-04
Channels
- # announcements (6)
- # babashka (5)
- # beginners (57)
- # biff (6)
- # business (32)
- # clj-together (1)
- # clojars (10)
- # clojure (56)
- # clojure-europe (76)
- # clojure-nl (4)
- # clojure-norway (40)
- # clojure-serbia (1)
- # clojure-spec (5)
- # clojure-uk (10)
- # clojurescript (3)
- # cursive (12)
- # data-science (1)
- # datascript (4)
- # datomic (35)
- # docs (4)
- # emacs (28)
- # events (5)
- # hyperfiddle (9)
- # matrix (1)
- # off-topic (28)
- # practicalli (4)
- # re-frame (14)
- # shadow-cljs (2)
- # testing (5)
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)
.
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.
Pretty much. I can easily use a map and provide the wrapped methods as metadata, but unfortunately I lose access to the other methods.
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
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
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
?
I don't know in advance what methods will be on the object, only that it will at least implement a particular protocol.
No, all I can guarantee is that it will implement a particular protocol. It might implement many others, or have java methods like .close
.
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.
Though perhaps it’d be possible to build those protocol and method definitions via reflection?
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
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. 😕
Reflection is interesting... Is it considered reliable? All I know is that I saw an option "warn on reflection" in a few projects.
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
Alright, thank you very much for your help!
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.
> 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.
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.
Hum, reify wouldn't lose the other methods, it will just overwrite them. Though now I'm not sure about the protocol extended methods.
I want to augment some protocol methods, in the same way as we can add middleware in web handlers.
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"))
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.
And your target object is a Java object? Or some clojure data which supports metadata?
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"
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.
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.
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?
(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.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.
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.
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
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')
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?
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
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 workingCaveat: 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)?
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
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)
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 samebut I need both the datasources, read-only for historical API and read-write for all the other ones
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?
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.
Some examples may help
Is state-flow
a library?
It's an integration test framework https://github.com/nubank/state-flow
neat! I hadn't heard of that library.