I mean the Type itself, in the sense of having a wrapping structure around the return.
Like with invite-user-to-project!
, in Clojure you could easily have that function return different things, maybe it returns true
if succeeded, nil
if missing user, :no-project-found
if missing the project, etc.
Though I’ve done things like wrap in a vector or a map often, but that’s also a lot less ceremony than creating a custom result type.
I’ve also seen this in the past and thought about it for some cases: GitHub - lambdaisland/uniontypes: Union Types (ADTs, sum types) built on clojure.spec but never really felt the need for it I guess.
Hadn’t seen keechma/pipeline before, but I had seen plumbing, though never tried it. I think my issue at first glance with both of them is that they seem a little limiting still.
Plumbing I don’t see how you’d setup conditional and recursive flows. With let
you can do:
(let
[foo (get-something)
bar (case foo :buzz (do-buzz) :bazz (do-bazz)
biz (save foo bar)]
{:result biz})
Which eventually I’ve found I need to do some conditionals in my flows at some point.
Or for recursive flows like:
(let
[foo
(loop [foo (get-something) attempt 3]
(if (and (nil? foo) (pos? attempt))
(recur (get-something) (dec attempt))
foo))
bar (case foo :buzz (do-buzz) :bazz (do-bazz)
biz (save foo bar)]
{:result biz})
And for keechma/pipeline it seems that you can’t choose prior results to pass to the next, it’s always the previous result passed to the next function. So say you want to do:
(let
[thing (get-thing)
other-thing (get-other-thing)
foo (do-something thing other-thing)
baz (save foo)]
{:result baz})
I guess you can maneuver around it either by using the pipeline state or by threading a map, not sure if there’s downsides to that.
In general, I’ve thought about this kind of thing before, I’m sure there could be something ergonomic, but it’s a balance between using standard Turing complete Clojure to model whatever flow you want, or come up with something a bit more structured/frameworky that lets you do it possibly more conveniently and declaratively with maybe some nice features like custom scheduling of steps and all that, but at the detriment of maybe being more limiting in some ways, or simply being more cryptic and requiring people to first learn the “framework”.
In a single threaded non-blocking context like ClojureScript I can see more value in such a thing, since using normal Clojure to describe these asynchronous flows do require a lot more work and the code can get unruly. But for threaded blocking contexts like Clojure I’m not sure it’s worth it.
That said, I think it’s a matter of experimenting and maybe someone will find something that has really nice ergonomics and isn’t limiting and plays nice with the rest of Clojure.
Another approach to this is, instead of defining what I call a “flow”, or a “workflow”, which I consider a “pipeline” a subset of. You can instead model a state machine.
So like instead of saying do this, then that, then depending on do this or that (like with let). Or instead of saying do this but require that to happen first, which requires that other thing (like with plumbing). What you would do is do something and return a transition and feed that to your state-machine. I haven’t tried it, but it be an alternative.