I think it would be interesting if the people who say that 20 effects is too much could tell me what they would do with the example I gave.
My example is not “public-facing API”, at least, it’s an internal function. But there are plenty of those.
I think it would be interesting if the people who say that 20 effects is too much could tell me what they would do with the example I gave.
My example is not “public-facing API”, at least, it’s an internal function. But there are plenty of those.
Perhaps, but no one can reason about functions in IO more ![]()
“abusing the effects system” implies that there’s a right and wrong way to use it and I don’t think there is (at least not in the way you’re suggesting).
I think it would be interesting if the people who say that 20 effects is too much could tell me what they would do with the example I gave.
This one? From the perspective of an OOP language, framework (like, say Spring Boot for Java):
Most components would not have a direct dependency on a database transaction component. Instead, decorators/proxies in combination with thread-local storage would be used to manage transactions transparently to the component. I tried to imitate that in Haskell here.
Similarly, open telemetry tracing could be added by a decorator/proxy (although if you need to emit very specific telemetry, a proxy might not be enough).
Effects like Retry or Time would only appear in the signature/constructor of components that actually retry or get the current time. Ditto for the AWS effect. Those constraints would not propagate transitively! That is: if component A uses component B and B uses Retry, A’s constructor would require a B, B’s constructor would require a Retry, but A’s constructor would not require a Retry.
Log would likely still be required by all components.
Stepping back a little, why is having 20 effects in a funcion too much? I can think of a few reasons:
One might argue that a 20-effect signature serves as a kind of documentation of all the effects taking part in the application. But that information could be tracked by other means, for example using a dependency injection container. Constraints aren’t that good for that task! They are not first-class values, they can’t be (easily) printed or listed.
Naturally it’s quite hard to do so in an accurate way, because you know the fine details of how these things are used and the rest of us don’t but I can make some suggestions. Ultimately my perspective on this derives from the principle of “you should structure your effects code using the same approach you would structure any Haskell code”. Specifically, I would give the same answer to your question if instead of “20 effects is too much” rather it was “20 arguments is too much”. That is, I would predominantly look for opportunities to bundle arguments into products containing things that are mostly used together. I would also look for the opportunity to creat a coarse-grained App effect.
Here are some thoughts about each effect:
IOE :> es -- standard from effectful
Once you have IOE all bets are off. All effect safety has gone out of the window. I would fight very hard to not have IOE in my signature, but without knowing more what you use it for I can’t suggest an alternative.
Statement :> es -- DB statements
DB :> es -- DB transactions
Bundle these together into one effect?
Concurrent :> es -- standard from 'effectful'
StructuredConcurrency :> es -- ki compatibility via 'ki-effectful'
Labeled "backgroundTasks" (Reader Ki.Scope) :> es -- ki scope for background tasks
Bundle these together into one effect?
Retry :> es -- retrying via 'retry-effectful'
Time :> es -- time access via 'monad-time-effectful'
I’m a bit confused why Retry is even an effect. Maybe because it has the ability to delay before retrying? In which case, maybe bundle Retry and Time?
-- 4 internal effects relating to specific computer vision tasks
Bundle these together into one effect?
-- 3 internal effects relating to global subsystems
Bundle these together into one effect?
Resource :> es -- resourcet via 'resourcet-effectful'
I’m surprised Resource is an effect. It’s behaviour should not be externally visible, much like bracket. In any case, I don’t think a good effect system should be requiring ResourceT. See Bluefin streams finalize promptly.
2 minor utility custom effects
Bundle these together into one effect?
AWS :> es -- AWS
Trace :> es -- opentelemetry tracing
Seems like these should be abstracted into a less-specific effects? If you include these then your effectful function is coupled to AWS and opentelemetry.
Log :> es -- `log-base` compatibility via 'log-effectful'
Perhaps could be bundled with some of the others into an App effect?
Writer T :> es -- locally used to track work we didn't finish
This one seems to genuinely belong on its onw.
I tend to disagree. IO is not specific, which is why it’s much easier to reason about it. We treat functions in IO with utmost care and assume nothing.
Whereas with 20+ effects, I have no idea what properties I can derive from that collection.
I’d probably just use IO at that point.