Fork me on GitHub
#polylith
<
2022-02-10
>
Rikard Mustajärvi17:02:53

Hi! Really liking Polylith. In interface.clj, would there be any reason not to just use a def which gets assigned to the function getting forwarded? Instead of creating forwarding/proxy functions you just assign the function do a def with the same name. Example:

(def create-signed-token jwt/create-signed-token)
instead of
(defn create-signed-token [token subject]
  (jwt/create-signed-token token subject))

seancorfield17:02:07

A couple of reasons: the main one being REPL-friendly development (the def binds whatever is the current implementation function and won't pick up updates to the implementation unless you also re-eval the def in the interface`.

seancorfield17:02:52

The other reasons include editor/linter support so they see a function definition, not just a Var; docstrings -- my interface docstrings target users of the component, my implementation docstrings target maintainers of the component; clarity -- using defn in the interface clearly signals a function, using def doesn't indicate whether it's a Var in the implementation or a function.

🙌 1
tengstrand07:02:00

I think this answer was so good, that I suggest that we add it to the documentation.

Ferdinand Beyer07:02:55

The real lazy might want to check out potemkin/import-vars: https://github.com/clj-commons/potemkin

Ferdinand Beyer07:02:31

But I would also go with @U04V70XH6’s advise 🙂

Hukka09:02:23

I wonder when I get an intuitive understanding of references in Clojure, so I wouldn't need to google how defing a var that refers to other var works differently than defing a function that uses another var

Hukka09:02:48

I also find making the wrapper functions in the interface annoying boilerplate

Hukka09:02:06

In this case good enough tooling that reloads all affected namespaces has kept me using the def way too, instead of the recommended defn

seancorfield10:02:17

I think you probably know my feelings on reload/refresh workflows by now... 😁

Hukka11:02:07

I don't, actually… But I admit that I more often just evaluate the top form, or a buffer, instead of saving to a file myself. Must be that I haven't used Polylith enough to have that bite me unexpectedly

Hukka11:02:36

Well, we don't even have a file watcher. Would have to manuall call the reload from repl

Rikard Mustajärvi11:02:23

Thanks for your comments. Will take the REPL-friendliness into consideration, but still not 100% convinced it's a bad shortcut :). Re: docstrings: What about using docstrings with defs?

Rikard Mustajärvi11:02:24

@U04V70XH6, do you have a blog post or similar about your opinions on reload/refresh workflows? Would be interested in reading it in that case.

Ferdinand Beyer15:02:36

@U02G4GV5Y9L - search slack for from:@seancorfield reload 🙂

seancorfield06:02:36

"search slack for from:@seancorfield reload  🙂" -- oh... gosh... I really am on a bit of a campaign about it, aren't I? 🙂 So many results for that search...

😁 1
Pieter Koornhof11:02:56

I echo @U04V70XH6 sentiments. I was on my own moving from .net to clojure and at that point have yet to have unlearn many things. So reloaded workflow seemed like a good idea at the time and we paid for that for quite a few years after. Having multiple different apps running on integrant on polygaul finally made us pull the plug on integrant and reloaded workflow. No looking back. Coincidentally we ran into some other interesting “state” related issues too. Like extending compojure response Renderable differently in different apps broke things in unexpected and hard to find ways.

Pieter Koornhof11:02:13

using def’s instead of defn means you always have to re-evaluate 2 things instead of one. which is why you then want to reach for something that is seemingly convenient.

Ferdinand Beyer11:02:38

Would actually be curious to learn more about your experience @UC465C762. I understand that “reload” is overused, and it is preferable to write code that allows you to simply re-evaluate pieces without having to restart. On the other hand, I sometimes find myself in the situation where I either forget to re-evaluate, or some var I deleted from the code is still there and causes problems. Like a failing deftest that keeps failing after removing the code picard-facepalm In these situations, I found being able to reload to be quite helpful actually.

Pieter Koornhof11:02:56

Sure. What I’m saying is not that reload is bad, but reloaded workflow. Also I wont say it is bad, I’d rather say it’s not needed. Tools for reloading all the things exist in the clojure.tools.namespace.repl namespace and does all the reloading you might need. What I don’t want in my regular workflow: 1. Change some code (optionally re-eval it) 2. Go to some other place/context and either reload or re-eval 3. Go to the calling code and continue what I was doing Instead I want 1. Change some code and re-eval it right there 2. Go to the calling code and continue what I was doing Not sure if the above makes sense or not. But the middle step is not really needed every time. And most of the time you will know when things are in a disjointed enough state for you to have to reload everything. This stems to other things beyond code reloading. During dev, rather read from .edn files every time instead of storing the value in a def. Otherwise you have to change the file and re-eval the def. I’ve seen time and again how that reload step catches people off guard and confuses them about how the system and clojure just works. And then they tend to move to a more reload and test workflow vs a repl driven workflow

Ferdinand Beyer11:02:07

Thanks. Makes sense! What did you end up doing instead of using integrant?

Ferdinand Beyer11:02:11

> This stems to other things beyond code reloading. During dev, rather read from .edn files every time instead of storing the value in a def. Otherwise you have to change the file and re-eval the def. Good point as well. It might come with the complexity of having to maintain two paths, e.g. load-once during production and load-every-time during dev. Isn’t that also something tools like integrant can help with?

Pieter Koornhof15:02:41

What did you end up doing instead of using integrant? I want to answer this with We ended up just using clojure but I guess that’s not helpful 😄 We ultimately used integrant for 4 different sets of problems 1. loading namespaces 2. combined with dyn-edn for getting at environment variables 3. starting and stopping things that start and stop 4. switching implementations (to stub out various 3rd party services and allow offline dev) We now do 1. we just require namespaces. pretty simple 😄 2. we use dyn-edn directly on .edn files via a specific config component 3. integrant also needs code to start and stop, we now just call it explicitly 4. we have a component that allows us to easily define and switch implementations (using protocols under the covers). With integrant, a change to any of the above required a reloading of all 4. Also with integrant, there where 2 steps to having an active system. Start the repl, then start integrant. Now once the repl is started you can interact with the actual system. The system is considered on always and you can just swap implementations in and out as you need and only start the bits you need when you need them instead of everything always. Removing integrant also means new people have one less thing to learn and understand. Honestly it’s not really that integrant is good or bad. It’s just that less is more, and having it in our project caused a bunch of extra things to exist, which is not helpful. It’s important to note that (especially with polylith), we don’t have this massive dependency graph of components that needs starting and stopping in a certain order. Everything we start and stop is independent of each other. It’s also worth noting, in prod we just start things. But in dev we have the need to start and stop things, for that we just use mount. but it’s only used in the dev project and not a thing baked into the namespaces etc. This is a mouthful. One last thing though. Because these things separately now, we where able to with ease build a small ui that just tells you the state of the system as well as allow us to easily run repl commands by clicking buttons on the ui.

Ferdinand Beyer15:02:56

Sounds good! I might need to double-read your message to get it right 🙂 One thing that many frameworks/libraries out there are missing in my opinion are a solution for dependency inversion. I am also not happy with how Polylith does it (copy-pasting interface namespaces?). Integrant, while I’m sure has its flaws, at least has a clean answer to that. Looks like you are running your own to achieve exactly that — this “component that allows us to easily define and switch implementations”?

Ferdinand Beyer16:02:43

Thanks for sharing!

Pieter Koornhof16:02:32

I’m not a fan of how polylith solves dependency inversion either (using aliases). We use protocols for this and we put the protocol behind the namespace. So the caller just imports the db namespace as an example and works with it without having to care about implementations or dependency inversion or whatever. Coming from a .net background I grappled with this same problem for a long time and it is the main reason I started using integrant in the first place. But the way we used it was not clojure idiomatic. And now that I feel we know a better way, I don’t feel I need integrant anymore. Does not mean it’s not good or anything. It’s just not valuable to us anymore. With our protocol bits we can swap stuff in and out without having to start or stop anything. Thinking about the onion and dependency inversion always struck me as this hard thing to learn in an OOP environment. I spent years trying to understand it and then get it right and the teach it to people. it’s not natural. Directly depending on thing and using them is natural. Clojure I feel hits the sweetspot with this. The code is very much one way when you read it. What you see is what you get. Just call the function in the namespace. But you can still have all the dependency inversion you want. Just without the effort. It took me a few years to unlearn a bunch of things.

Pieter Koornhof16:02:01

And you can most definitely use integrant for this

Pieter Koornhof16:02:02

we just don’t have the need anymore

Pieter Koornhof16:02:42

I’m trying to convey a lot of info in a short amount of time 😛 So if you are keen we can just get together via video chat or something and I’ll show you what we did.

Ferdinand Beyer16:02:03

So you are basically doing something like:

(defprotocol Interface ...)

(def impl nil) ;; TODO: bind to actual implementation

(defn public-func []
   ...delegate to impl as needed...)
?

Ferdinand Beyer16:02:35

> So if you are keen we can just get together via video chat or something and I’ll show you what we did. Sure, always happy to get inspired 🙂

Pieter Koornhof17:02:20

Yes re protocol impl. with some added niceties to allow witching the impl dependency inversion style.

Pieter Koornhof17:02:52

not sure if you’ll be inspired 😄 but talking about this stuff makes me happy

Ferdinand Beyer17:02:13

Well I think I got the gist of it 🙂

seancorfield18:02:08

I'll add that even clojure.tools.namespace.repl is really not needed if your REPL-based workflow is tight. That just takes practice and discipline -- always eval any top-level form you change, before you even move focus away from that code (you don't even need to save the file to eval a form). But it is very different from working with other languages and people tend to fight against it because it is such a different workflow.