clojure-losangeles

nate 2025-11-15T05:10:36.612859Z

One approach that we talk about in the podcast is having a loop that alternates between I/O and pure logic. The point is fully made in https://clojuredesign.club/episode/024-you-are-here-but-why/, but the rest of that series gives more context. In the case of exceptions, that’s just another output from I/O that you can hand to pure logic to get a decision on the next step. I have a recent iteration of it that I can share after a bit of cleanup. I’m curious as to Sean’s suggestions as well.

seancorfield 2025-11-17T16:07:31.097649Z

I'm always looking for clean ways to separate side effects and logic but I'm not sure that turning the process into a state machine is particularly clear since it's much harder to "see" the actual flow at a glance. What I have done sometimes is to split each step into a function (same as the podcast) but explicitly thread state through a fixed set of steps, using an :error key in the state to indicate an exception occurred, and each step function has a condition on the state (likely anyway since any conditionals will already be checking the state to make a decision).

(-> init-state
  (some-logic)
  (some-io)
  (more-logic)
  (more-io)
  (final-logic))
We have a condp-> macro at work that let's us lift the "ok?" check into the threading flow if that makes things clearer -- it's like cond-> except the value is threaded through the predicate as well as the expression.

nate 2025-11-17T17:16:45.167169Z

I totally agree about the degradation in glanceability that a state machine causes. I used multimethods to implement a state machine in a service a few jobs ago and while it worked well, it was easy to get lost because all I could see at any one time was one step in the process. To see the rest, I had to jump to other parts of the namespace. The benefit of a state machine is that you can advance non-linearly (go back to the start or retry a step) and it's the same as advancing to the next step. That's the only criticism of the threader example is that you can't go back a step if the I/O result necessitates it (like for a retry). Cool that there's a condp-> macro out there. I would like the option to have the value threaded as well.

seancorfield 2025-11-17T17:52:33.312699Z

https://github.com/worldsingles/commons/blob/master/src/ws/clojure/extensions.clj#L24 We have a with-retry macro at work to wrap code in "retry on exception" logic. We can specify initial backoff and max retries etc.

nate 2025-11-17T18:22:39.623229Z

Cool, thanks for sharing.

2025-11-23T09:14:40.029479Z

Interesting discussion. It seems like there's at least two approaches here that involve still returning something on exception via wrapping - that almost sounds a bit like monadic error handling. a) am I right? b) could one of the monads libraries for Clojure be helpful in the situation we're discussing here?

seancorfield 2025-11-23T17:42:00.788359Z

Given Clojure's type system is dynamic, I do not find monadic code very readable in Clojure personally (compared to how monads work in Haskell, for example). That said, when you have any sort of pipeline with error handling encoded alongside non-error values, you are typically in monadic-style code even if you are not using, say, org.clojure/algo.monads. Several people (myself included) have tried to create libraries around this sort of pattern (pipeline of left/right monadic style data) but my experience has always been that the code is made less readable by the "magic" involved and the strict compliance to that monadic style. Very much a YMMV situation tho', and there are def. folks who prefer the monadic style of code.

2025-11-23T17:49:36.579349Z

That's really valuable input, thank you! I had a feeling that trying to impose strict monads onto Clojure would be a bit unwieldy, good to know that I wasn't entirely wrong there

2025-11-16T14:30:53.927979Z

I'll give the episode another listen (or two or three). I think I remember this one: an engine that drives state around and around, dispatching to different deciders and workers. I think what's still missing for me is how to lift the exceptions out. If a SQL exception is thrown, I can decide on which to catch based on some exception type code, and then need to decide how to handle that. If I just blanket wrap as data, then SQL information leaks to a higher layer. If I'm selective, then I've got some catch "logic" in the io that I can't test without mocking the db access

nate 2025-11-16T23:40:28.481179Z

The approach I've taken when doing this is to wrap the exception in a map as data and hand that back to the pure logic side. In the case I mentioned above, any exception is handled the same way (i.e. redirect to the same page), so I don't need to unpack it there. If I did, I would probably use instance? to check for a couple specific classes and handle accordingly. It does mean the exception is seen outside of the side-effecting function. It's part of the data that represents the result of that function.

👍🏻 1