Custom monad with servant and throwing errors
In the past I've always used scotty when writing web services. This was mostly due to laziness, I found working out how to use scotty a lot easier than servant, so basically I was being lazy. Fairly quickly I bumped into some limitations in scotty, but at first the workarounds didn't add too much complexity and were acceptable. A few weeks ago they started weighing on me though and I decided to look into servant and since I really liked what I found I've started moving all projects to use servant.
In several of the projects I've used tagless final style and defined a type
based on ReaderT
holding configuration over IO
, that is something like
newtype AppM a = AppM {unAppM : ReaderT Config IO a} deriving ( Functor, Applicative, Monad, MonadIO, MonadReader Config ) runAppM :: AppM a -> Config -> IO a runAppM app = runReaderT (unAppM app)
I found that servant is very well suited to this style through hoistServer
and
there are several examples on how to use it with a ReaderT
-based type like
above. The first one I found is in the servant cookbook. However, as I realised
a bit later, using a simple type like this doesn't make it easy to trigger
responses with status other than 200 OK
. When I looked at the definition of
the type for writing handlers that ships with servant, Handler
, I decided to
try to use the following type in my service
newtype AppM a = AppM {unAppM : ReaderT Config (ExceptT ServerError IO) a} deriving ( Functor, Applicative, Monad, MonadIO, MonadReader Config ) runAppM :: AppM a -> Config -> IO (Either ServerError a) runAppM app = runExceptT . runReaderT (unAppM app)
The natural transformation required by hoistServer
can then be written like
nt :: AppM a -> Handler a nt x = liftIO (runAppM x cfg) >>= \case Right v -> pure v Left err -> throwError err
I particularly like how clearly this suggests a way to add custom errors if I want that.
- Swap out
ServerError
for my custom error type inAppM
. - Write a function to transform my custom error type into a
ServerError
,transformCustomError :: CustomError -> ServerError
. - use
throwError $ transformCustomError err
in theLeft
branch ofnt
.
A slight complication with MonadUnliftIO
I was using unliftio in my service, and as long as I based my monad stack only
on ReaderT
that worked fine. I even got the MonadUnliftIO
instance for free
through automatic deriving. ExceptT
isn't a stateless monad though, so using
unliftio is out of the question, instead I had to switch to MonadBaseControl
and the packages that work with it. Defining and instance of MonadBaseControl
looked a bit daunting, but luckily Handler
has an instance of it that I used
as inspiration.
First off MonadBaseControl
requires the type to also be an instance of
MonadBase
. There's an explicit implementation for Handler
, but I found that
it can be derived automatically, so I took the lazy route.
The instance of MonadBaseControl
for AppM
ended up looking like this
instance MonadBaseControl IO AppM where type StM AppM a = Either ServerError a liftBaseWith f = AppM (liftBaseWith (\g -> f (g . unAppM))) restoreM = AppM . restoreM
I can't claim to really understand what's going on in that definition, but I have Alexis King's article on Demystifying MonadBaseControl on my list of things to read.