I've had some success with a rather horrible hand written monad that uses something like

newtype M r a = M { runM :: r -> (# Bool, a #) }

where I treat the Bool like the Maybe constructor and in the Nothing case put an error in for 'a'. I usually use this when I have more structure (an environment e, state, logs, etc.), so I'm not sure how well it would pay off when it is this simple but the monad looks something like:

instance Monad (M r) where return a = M (\_ -> (# True, a #)) M f >>= k = M (\r -> case f r of (# True, a #) -> runM (k a) r (# False, _ #) -> (# False, undefined #)) fail _ = M (\_ -> (# False, undefined #))

This has the benefit that we don't construct any thing on the heap, just the stack.

However, you need to be careful to be strict in all the right places. It is easy to accidentally build a thunk in your state which can kill your performance.

If you are feeling daring you can smuggle an unsafeCoerce d error through in the 'a' slot on failure as well and extract it at the end, or you can just translate the Bool into a Maybe e but you need to be careful, because you don't want to build up a tower of unsafeCoerces defeating all the work you went through to get this far.

The effectiveness of this approach depends on how much the overhead of building and tearing down Maybe frames is vs. the mental and execution time costs of distributing the code for dealing about failure across a lot of different places in the code. Note how >>= has to effectively unwind the Failure case manually.