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
RetryorTimewould only appear in the signature/constructor of components that actually retry or get the current time. Ditto for theAWSeffect. Those constraints would not propagate transitively! That is: if componentAuses componentBandBusesRetry,A’s constructor would require aB,B’s constructor would require aRetry, butA’s constructor would not require aRetry. -
Logwould 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:
- It obscures the signature of business-logic-implementing functions with low-level details like database access, when those signatures should, ideally, communicate dependencies on other business-logic-related functions.
- Because of the transitive accumulation of effect constraints, changing a function deep in the call stack to add or remove an effect might trigger a nightmarish chain of refactors in the signatures of all functions above.
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.