-->

Pure error handling in Haskell with Either: how to

2020-07-09 09:26发布

问题:

I'm mainly interested in the Either monad and all it's uilitites from Control.Error. Reading errors-1.0: Simplified error handling, I got convinced pure errors should be kept apart from IO errors. This means error, fail, exitFailure are functions whose use should be reduced to the IO monad. Pure computations can and will create condition errors, but are deterministic.

Currently, working with folds, I got a situation an element in an array may generate a condition error which makes the whole computation unsatisfiable. For example (using Data.ConfigFile):

type CPError = (CPErrorData, String)
data CPErrorData = ParseError String | ...
type SectionSpec = String
type OptionSpec = String

instance Error CPError
instance Error e => MonadError e (Either e)

get :: MonadError CPError m => ConfigParser -> SectionSpec -> OptionSpec -> m a


dereference :: ConfigParser -> String -> Either CPError String
dereference cp v = foldr replacer v ["executable", "args", "title"]
 where
  replacer :: String -> Either CPError String -> Either CPError String
  replacer string acc = do
    res <- acc
    value <- get cp "DEFAULT" string
    return $ replace ("${" ++ string ++ "}") value res

The situation is: I'm using an acc which has a complex type, just because if a single element isn't found doing the replacement, then the whole value isn't calculable.

My question is: Is this ugly? Is there a better way of doing this? I have some nastier utilities of type EitherT CPError IO String in the acc because of some IO checks.

回答1:

I now understand I was looking into a way to fold a list of composable actions. I came across this question, and learned about the Kleisli operator:

dereferenceValue :: ConfigParser -> String -> Either CPError String
dereferenceValue cp v = do
  foldr (>=>) return (fmap replacer ["executable", "args", "title"]) v
 where
  replacer :: String -> String -> Either CPError String
  replacer string res = do
    value <- get cp "DEFAULT" string
    return $ replace ("${" ++ string ++ "}") value res

Probably, this seems a bit like my question, but it feels cleaner. Particularly because of the signature of replacer. It doesn't receive a Monad as a second parameter and becomes more useful for other parts of code.

EDIT:

Even easier:

dereferenceValue :: ConfigParser -> String -> Either CPError String
dereferenceValue cp v = do
  foldM replacer v ["executable", "args", "title"]
 where
  replacer :: String -> String -> Either CPError String
  replacer res string = do
    value <- get cp "DEFAULT" string
    return $ replace ("${" ++ string ++ "}") value res

Conclusion: learn to use Hoogle



回答2:

As you discovered, foldM works very nicely here. Your replacer function

replacer :: String -> String -> Either String String
replacer res string = do
  value <- get cp "DEFAULT" string
  return $ replace ("${" ++ string ++ "}") value res

can be further beautified using Applicative as follows

replacer :: String -> String -> Either String String
replacer res string =
  replace ("${" ++ string ++ "}") <$> get cp "DEFAULT" string <*> pure res