Suppose you have a few modules in an application which needs to request and parse data from a JSON api:
- Http
- Json parsing
- Business logic, something to do with Images
Each of these can fail in their own way:
- Http errors -- SocketError, Timeout, NotFound, BadGateway, EmptyBody
- Json parsing -- MissingField, UnexpectedType
- Business logic -- WrongDimensions, WrongFormat
As much as possible, you want to these modules to be independent. The image validation shouldn't know or have anything to do with Http details. (An image however will need to be able to be constructed from JSON.)
Each module will need it's own error data type to capture its particular failures. Here's a sketch of these modules, the error data types and the functions that use them:
module Http where
data HttpError = SocketError | Timeout | NotFound | BadGateway
execute :: Request -> Either HttpError Response
module Json where
data JsonError = MissingField String | UnexpectedType String String
parse :: JsonParser a -> String -> Either JsonError a
module Image where
data ImageError = WrongFormat | WrongDimensions
validateImage :: Image -> Either ImageError Image
parseImage :: JsonParser Image
What happens when we try to glue these components together in our app?
ex1 :: Request -> Either ??? Image
ex1 imageRequest = execute imageRequest >>= parse parseImage >>= validateImage
It won't compile. Each of our modules use a different error type, so we cannot bind between them. We need to do 2 things:
- Create an application-level error data type, that can hold any of the errors for any of the subcomponents
Lift
each function in each component so that it produces errors that are wrapped up in our application level data type
data AppError = H HttpError | J JsonError | I ImageError
-- given, first :: (e -> q) -> Either e a -> Either q a
liftHttpError :: Either HttpError a -> Either AppError a
liftHttpError = first H
liftJsonError :: Either JsonError a -> Either AppError a
liftJsonError = first J
liftImageError :: Either ImageError a -> Either ImageError a
liftImageError = first I
Now our request pipeline looks like this:
ex1 :: Request -> Either AppError Image
ex1 imageRequest = liftHttpError (execute imageRequest) >>= (liftJsonError . parse parseImage) >>= (liftImageError . validateImage)
-- Again with do notation, just for comparison
ex1' :: Request -> Either AppError Image
ex1' =
do r <- liftHttpError (execute imageRequest)
j <- liftJsonError (parse parseImage r)
liftImageError (validateImage j)
Okay! So far not too bad. We have modules that are independent (they have with their own specific error types, and compile independently of one another), and when we compose up at the application level we have a mechanism for bringing everything together.
But, consider the case where we want to factor our application into 2 parts: one that does the http request (liftHttpError (execute imageRequest)
), and the other half that parses the json and validates the image ((liftJsonError . parse parseImage) >>= (liftImageError . validateImage)
).
p1 :: Either HttpError Request
p1 = execute imageRequest
p2 :: Request -> Either ??? Image
p2 r = liftJsonError (parse parseImage r) >>= (liftImageError . validateImage)
So we're back a step again. We could give p2
an error type of AppError
, but that includes the possibility of failure with HttpError
.
Update should answer your 1st question :)
Re 204: For this example it doesn't really matter, it could be http requests and response, or it could be some other protocol and it wouldn't change very much. It's still up to you to lift the protocol up into your own apps data types. ie. escape the protocol early. In the http libs I've used 204 just means you have empty body. So maybe you would parse it to
()
?With async, I would add promises/futures/asyncs and wrap it that way.
This is the
EitherT
transformer:EitherT is a Monad/Applicative/Functor and more, so you get a whole lot of functions to use out of the box.
If you go this way, you need to complicate your lift functions again to add the Async if it doesn't exist yet. i.e.
liftJsonError :: Either JsonError a -> EitherT AppError Async a
. This post is slowly working it's way towards a neater solution to that too :)