Fork me on GitHub
#clojurescript
<
2024-04-03
>
madis21:04:07

I'm building a ClojureScript wrapper for a JS library. Basically all the methods exposed are in one namespace. I would like to be able to offer alternative, no-op implementation for all these methods so the user can turn it off for example during testing. What would be an idiomatic way to do it in Clojure, that doesn't require re-compilation? In other words the user could switch to a different implementation just by starting the app with different configuration parameter. An idea that came to my mind was to implement the same functions in another namespace but this can lead to the signatures (arg lists) getting out of sync and functions too (being present in one but not in another ns). Can you recommend better ways?

(ns api.doing-work)

(defn send-message [content]
  (http/post "/messages" {:content content}))

(defn send-alert [content]
  (messaging/alert content))

;; And in another file
(ns api.no-op)

(defn send-message [content])
(defn send-alert [content])

p-himik21:04:36

There are no magic solutions here. You can use a macro or a namespace swap - they both require recompilation to take effect. The latter can also lead to the signatures issue that you've pointed out. You can use a swappable function registry or the simplest thing - runtime conditions. These approaches can be implemented in a way that doesn't require recompilation.

p-himik21:04:16

Although the signatures issue can be alleviated by simply making all functions in no-op having [& _] signature. But of course, that wouldn't fix the issue of missing or extra functions. And it still require recompilation to swap namespaces.

madis21:04:53

I see. Thanks for the ideas! If there are more, I'm all ears 🙂

phronmophobic21:04:48

there's a few methods (some are more annoying cljs). My favorite is something like:

(ns api.imessage)
(defprotocol IMessage
  (send-message [_ content])
  (send-alert [_ content]))


(ns api.doing-work)

(defn send-message [content]
  (http/post "/messages" {:content content}))

(defn send-alert [content]
  (messaging/alert content))

(def messenger
  (reify
    IMessage
    (send-message [_ content]
      (send-message content))
    (send-alert [_ content]
      (send-alert content))))

;; And in another file
(ns api.no-op)

(defn send-message [content])
(defn send-alert [content])

(def messenger
  (reify
    IMessage
    (send-message [_ content])
    (send-alert [_ content])))

(ns other.ns)

(def messenger (if foo work/messenger no-op/messenger)

(api.imessage/send-message messenger content)
• Users who only care about using a single implementation can use the namespace directly if they want. • Implementing it via a protocol also provides a good place (eg. defrecord) to put config if that comes up in the future

hiredman21:04:24

I have a lot less experience on the cljs side of things, but in general I think clients written like this, that just directly do http posts etc, without reifying the client as a thing, are bad

hiredman21:04:11

A client should have something like a make-client function, and all functions that do something should take whatever make-client returns as an argument

hiredman21:04:51

A protocol and multimethod based approaches naturally work in that style

hiredman21:04:33

And it allows for things like multiple instances of the client configured differently to coexist

madis22:04:29

This is why I love the clojure community, always getting good ideas and solutions! @U7RJTCH6J I'll try that one! Haven't had to use protocols before (only read about them). And @U0NCTKEV8 thanks to you too! The examples I put there were imaginary to keep things short... I wouldn't do that in real app 🙂