Q: Designing library functions that have multiple effects

Let’s say we have a set of functions that each may incur a different combination of side effects, such as :

  • empty output
  • failed input parsing
  • failed output rendering

and others.
When combining such functions, I often find myself with “mixed” signatures, containing both constraints and concrete types, e.g. MonadThrow m => ... m (Maybe (Foo a)).
Let’s now say I’d like to provide a “batteries-included” solution to the library user, such that she will have to implement as little as possible in order to use my functions.
The “user experience” I foresee is : 1. user parses her own types into some opaque/abstract internal representation 2. user applies library functions (in-memory data munging, no I/O for now) 3. user parses IR back into her own result types.

Which is a better course of action, to package all effects in a newtype which has all necessary instances, or to defer all known failure modes into MonadThrow or similar ? What could be other design options?

I understand there are multiple tradeoffs to be made here:

  • whether should be able to “build on top” of the library types or not
  • how should the user be made aware of those side effects (exceptions? logging? one big AST output? etc.)
  • and others

Thanks for all suggestions!

3 Likes

I think it really depends on how many effects your library needs; but the “classic” MTL approach would be to have a typeclass MonadStuff and then a provide a concrete StuffT as well, and an easy way to run StuffT with some default parameters.

That being said, if step (2) in your explanation doesn’t really require I/O, I would try to design it as a pure function as much as possible.

1 Like