Fork me on GitHub
#clojuredesign-podcast
<
2020-02-19
>
markbastian01:02:06

Hey guys, I enjoyed the podcast this week. One question - I use (and like) the pattern of pouring my needed components (not the entire system) into my handler via a middleware for the simple reason that the ring handler by definition takes a map (the request) and returns a response. It is very easy with Integrant to integrate what is needed and keep the handler and routing logic separate from the means of adding the components. However, I totally agree with the downside that you now have “foreign keys” at the edges where you call the handler. You mentioned a pattern in which you use partial application to create a handler instead. Do you have examples of this? I’m wondering how this is done in a way that doesn’t capture system in the scope of the handler. Thanks!

nate01:02:48

Hey Mark. I'll see if I can extract a minimal example. I can't promise that it will be soon, I'll ping you.

markbastian01:02:12

Thanks! No worries on the timeline.

rgm06:02:27

@markbastian I hope I understood your question right, but this is what I've tended to settle on https://gist.github.com/rgm/775909ca19002591c8952b823327558d . I used to have one integrant component per handler, but that's getting a bit much. The art, as is often the case with fp, is in arranging your function args to make this pattern work well.

markbastian14:02:42

I see. This puts the global handler and router in the scope of the init-key function, though. I think this complects your system with router and handlers. For example, how would you do the mock/test I do here where I mix the component into the request (https://github.com/markbastian/conj2019/blob/master/src/main/clj/conj2019/horsemen/core.clj#L161-L169)?

rgm15:02:32

Ah. Yeah. In practice I keep my init-key fns themselves either a partial or an anonymous fn that only plugs it together, maybe 3 lines max. The whole router form is outside the integrant goop, so it’s fairly easy to just call that function in a test to make the big handler.

rgm15:02:29

I’ve been burned by enough frameworky frameworks that I try to keep that interface layer between my stuff and the framework extremely thin and obvious.

rgm15:02:18

So your handler line in that threading becomes (partial big-handler-fn {:conn conn}) I think... not sure I’ve got the arg order right. Anyway, yeah, no integrant in tests.

markbastian15:02:40

So you'd create your ring-handler in a function that takes a datasource as an argument and then partial that function when you init your system (essentially split out the body of init-key into its own function)?

rgm15:02:43

Yep. A side effect is that repl work is a lot easier too.

rgm15:02:27

You can manually start a component in mount, integrant, component, etc. but I find it simpler to just have a thin glue between those and a regular fn, instead of having big init-key or :start or what have you forms.

rgm15:02:12

Basically I never remember the API so I try not to have to.

markbastian15:02:14

Right, whatever solution you use should not be framework-dependent.

rgm15:02:18

Anyway I hope I understood your q.

markbastian15:02:50

At one point I used the positional args (versus the "enhanced request") approach using compojure and I had some challenges with it, but it might work better with reitit. There were some issues with scope capture and there's also the gotcha that if you don't partially evaluate the handler you will rebuild it every time you invoke the function. This had terrible performance when we used certain middlewares (ring-webjars, for example). I like my current approach, but I think I'll revisit the other as well. It may work much better with reitit.

markbastian15:02:16

Yeah, I just wanted to see an example of what the authors were thinking to make sure I wasn't missing anything.

rgm15:02:16

Well, I’m also hoping this is actually an example of that.

rgm15:02:44

Could be with reitit. I never got that good with compojure and hadn’t really internalized when things were happening given the macros, which made it tough for me to understand its interplay with component.

markbastian15:02:42

I believe it is. If the global handler is going to be generated via a function that takes in its stateful items I think it has to wrap the entire global handler unless you wrap your endpoints individually, but then you'd have a spaghetti code nightmare.

rgm15:02:33

So far I’m liking how the one global handler switchyard that partially hydrates simpler things into a full routing tree is working in practice. Add middlewares and it can turn into a lot of goop, but at least it’s in one place and usually fits on two halves of one screen.

rgm17:02:57

(I've updated the gist to incorporate this discussion about disentangling from whichever injection framework is in play).

mmeix14:02:56

Which in itself would be a subject: how to sensibly arrange your function args (just hinting … )

rgm14:02:11

I think that was one of my big FP moments, eventually understanding what someone meant when they said underscore.js and lodash got argument order wrong everywhere. It’s eg. _.map(coll, f) and so partial is ugly and involves placeholders.