Skip to main content

MTL Release (Part 3)

This morning, I had a bit of time to start making ginger compatible with the recent MTL release. Most errors are resolved by changing imports, but I ran into an error that cannot be fixed so easily. The current version of transformers removed the long deprecated ErrorT transformer, so code that makes use of ErrorT needs to be refactored.

The RegexMaker type class defined in the regex-base package has methods for creating regular expressions. The makeRegexOptsM method is the variant that allows you to specify options as well as reports errors using MonadFail.

makeRegexOptsM :: MonadFail m => compOpt -> execOpt -> source -> m regex

The use of MonadFail is unfortunate. The type of this function is equivalent to the following type. The function is pure and can return either an error message or a regular expression.

makeRegexOpts' :: compOpt -> execOpt -> source -> Either String regex

With such a simple type, one can easily use MonadFail by composing with either fail pure. It is unfortunately not as easy to convert from MonadFail to an Either type. The makeRegexOptsM function was previously used with the now deprecated ErrorT transformer, which has the following MonadFail instance. With this instance, any use of fail returns the error as an ErrorT error.

instance (Monad m, Error e) => Fail.MonadFail (ErrorT e m) where
    fail msg = ErrorT $ return (Left (strMsg msg))

Most uses of ErrorT can be refactored to use ExceptT instead, but the MonadFail instance of ExceptT simply delegates the fail call to the extended monad, so ExceptT cannot be used in this case.

instance (Fail.MonadFail m) => Fail.MonadFail (ExceptT e m) where
    fail = ExceptT . Fail.fail

Kazuki Okamoto created a package called either-result that wraps ExceptT in a type called ResultT that just changes the MonadFail instance to behave like the ErrorT instance. I could use this to resolve my issue in ginger, but I am not keen on adding a dependency for something so simple.

instance Monad m => MonadFail (ResultT m) where
    fail = throwE

Since the makeRegexOptsM function is pure, I do not even need to use a monad transformer. I think I will just use code like the following. Note that it requires the DerivingStrategies and GeneralizedNewtypeDeriving extensions.

newtype FailToEither a
  = FailToEither
      { runFailToEither :: Either String a
      }
  deriving newtype (Applicative, Functor, Monad)

instance MonadFail FailToEither where
  fail = FailToEither . Left
  {-# INLINE fail #-}

This is just a newtype wrapper around Either. It uses the existing Functor, Applicative, and Monad instances, and a MonadFail instance is implemented as Left.