Skip to content

Instantly share code, notes, and snippets.

@nkpart
Last active May 6, 2016 12:07
Show Gist options
  • Save nkpart/7777755b79cbfe6bd089 to your computer and use it in GitHub Desktop.
Save nkpart/7777755b79cbfe6bd089 to your computer and use it in GitHub Desktop.

Modules, Errors and Coupling

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.

@ryanbooker
Copy link

ryanbooker commented May 6, 2016

@johnmay10000 Yeah I think so. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment