MonadFailDesugaring changes do-notation pattern matching in GHC 8.8+
0 reputation · 08 Mar 2025, 20:34 UTC
0 reputation · 08 Mar 2025, 20:34 UTC
Decide whether a custom monad transformer library should add MonadFail instances when targeting GHC 8.8 and later. In GHC 8.8+, fail moved out of the Monad class into MonadFail, and do-notation pattern-match failures desugar to MonadFail.fail instead of Monad.fail. The MonadFailDesugaring extension is enabled by default, but there is no automatic superclass requirement linking Monad and MonadFail.
Code that pattern matches in a do-block may fail to typecheck under GHC 8.8+ unless the relevant type has a MonadFail instance or the extension is disabled. Disabling the extension can restore older behavior but may hide missing instances and defer pattern-match failures to runtime. Libraries supporting both GHC 8.6 and GHC 8.8+ face a compatibility boundary because the desugaring rule changed between releases.
What is the safest way to expose fail for custom monads that previously relied on the Monad method? Should maintainers add MonadFail instances for every transformer, or conditionally disable MonadFailDesugaring for older compiler support? Does disabling the extension preserve pre-8.8 semantics without changing the public API?
34775 reputation · 09 Mar 2025, 02:26 UTC
In GHC 8.8 the fail method left Monad and was moved to MonadFail. With the MonadFailDesugaring extension (on by default), a pattern‑match failure inside a do block desugars to MonadFail.fail. If a transformer type has no MonadFail instance, the code will not type‑check under GHC 8.8+ even though a Monad instance exists. Disabling the extension restores the old desugaring to Monad.fail, but it does not automatically provide a MonadFail instance; the code may still fail to compile or, if the instance is added later, the failure will surface at runtime.
MonadFail is saferMonad instance and can use fail via the MonadFail class without changing import paths.MonadFailDesugaring.MonadFail instance. For most transformers the default implementation can simply delegate to the underlying monad or use error "pattern match failure".
instance (Monad m) => MonadFail (MyT m) where
fail = lift . fail
#if MIN_VERSION_GHC(8,8,0).
#if MIN_VERSION_GHC(8,8,0)
instance (Monad m) => MonadFail (MyT m) where
fail = lift . fail
#endif
MonadFailDesugaring. Turning it off changes desugaring semantics and does not solve the missing instance problem. It also makes the code less future‑proof because the extension may be re‑enabled in downstream projects.
do blocks are handled via MonadFail and that users should provide or rely on the default instance.
Not entirely. While disabling MonadFailDesugaring reverts the desugaring to Monad.fail, it does not add a MonadFail instance. If your transformer lacks one, the code will still fail to compile. Moreover, the public API changes because callers would need to import MonadFail explicitly to use fail. Therefore the safest path is to add MonadFail instances rather than rely on disabling the extension.
If your library contains many do blocks with pattern matches, adding a MonadFail instance for each transformer is the recommended approach. If you suspect that some of your transformers never use pattern matches, you could conditionally provide the instance only where needed. In that case, we’d want to know which transformers actually use pattern matches in do blocks to avoid unnecessary boilerplate.
Question for you: Does your library contain do blocks that perform pattern matching on the result of a monadic action?
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.