Apologies in advance if I use the wrong terminology here.
What is the idiomatic way in Haskell to generalize two or more types so that you can defer pattern-matching against them while avoiding boilerplate code?
To give a concrete example: In my application I want to pass on possible errors that can happen during the execution. These errors are from a different module, so I don't directly control them:
data ErrorCatA = WTFError String | OMGError String
data ErrorCatB = BadError Int | TerribleError Float
Now I want to pass some form of supertype of these error categories on so I can handle them like this:
handleError :: GenericError -> IO ()
handleError err =
putStrLn $ case err of
WTFError s -> "WTF?! " ++ s
OMGError s -> "OMG?! " ++ s
BadError i -> if i > 5 then "Really bad" else "Not that bad"
TerribleError f -> "Terrible! " ++ show f
Is this possible?
I got closest by creating a wrapper type like this:
data GenericError = CatA ErrorCatA | CatB ErrorCatB
class GError a where
wrap :: a -> GenericError
instance GError ErrorCatA where
wrap = CatA
instance GError ErrorCatB where
wrap = CatB
By doing this I can wrap all errors easily, as in
handleError $ wrap $ WTFError "Woopsie"
but I would need to change handleError
to match against CatA (WTFError s)
etc.
Is there a simpler or more idiomatic way of dealing with a scenario like this?
Say you have the exception types
data HttpException -- from http-client package
data MyCustomError = WTFError String | OMGError String
data AnotherError = BadError Int | TerribleError Float
And you wanted to handle each individually, but generically. Instead of writing a sum type around them as
data AllErrors = A HttpException | B MyCustomError | C AnotherError
What you really want is to handle each exception. So why not just do that? Write the functions
handleHttpError :: HttpException -> IO ()
handleCustomError :: MyCustomError -> IO ()
handleAnotherError :: AnotherError -> IO ()
Then write a class
class HandledError e where
handleError :: e -> IO ()
With
instance HandledError HttpException where
handleError = handleHttpError
instance HandledError MyCustomError where
handleError = handleCustomError
instance HandledError AnotherError where
handleError = handleAnotherError
And just use handleError
where needed. It isn't really any different than what you have, but now the logic for handling one kind of error isn't mixed in with the logic for handling another kind of error. Just think of the class level handleError
as the same as your handleError . wrap
.
I'd go with creating a type class:
class GError a where
errorMessage :: a -> String
and provide meaningful instances to it:
instance GError ErrorCatA where
errorMessage (WTFError s) = "WTF?! " ++ s
errorMessage (OMGError s) = "OMG?! " ++ s
instance GError ErrorCatB where
errorMessage (BadError i) = "Bad! " ++ show i
errorMessage (TerribleError f) = "Terrible! " ++ show f
and use it like:
handleError :: GError a => a -> IO ()
handleError = putStrLn . errorMessage
Live demo
Of course the GError
instance is fully customizable. You can include any behavior that is both of ErrorCatA
and ErrorCatB
in your specific context.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With