First, the error monad: lets you sequence operations that may fail with error messages. There are a bunch of different types and type classes available so it's pretty general, but the one I always end up using is
MonadErrorinstance. Here's a contrived example:
import Control.Monad.Error -- suppose you have a function like: parseRomanNumeral :: String → Either String Int parseRomanNumeral input = -- code here; result in either (Left "parse error message") or (Right somenumber) -- equivalently, either (throwError "parse error message") or (return somenumber) -- then you can use the monad like this: addRoman :: String → String → Either String Int addRoman a b = do inta ← parseRomanNumeral a intb ← parseRomanNumeral b return (a + b)If either of those parses fail, then the result of
Leftwith the error. Otherwise, success is again
Right. Hopefully you can see this composes easily.
Ok, more background: you'll often mix this with IO. For example:
loadConfigFile :: FilePath → IO (Either String Config) loadConfigFile path = do contents ← readFile path -- parse parse parse. -- errors are: return (Left err) -- success is: return (Right ok) -- (again can use "throwError", but the success case then looks like -- return (return ok), which is sorta crazy) -- now imagine loading two config files: loadTwo :: IO (Either String (Config, Config)) loadTwo = do maybeconfig ← loadConfigFile "foo" case maybeconfig of Left err → return (Left err) Right foo → do maybeconfig ← loadConfigFile "bar" case maybeconfig of -- yuck! I won't even finish thisIt'd be nice to again compose these like I did in
addRoman. That's what the
ErrorTmonad transformer is for. Rewriting the above to use it, with the new bits underlined:
loadConfigFile' :: FilePath → ErrorT String IO Config loadConfigFile' path = do contents ← liftIO $ readFile path -- plain "throwError" and "return ok" now work here. loadTwo' :: IO (Either String (Config, Config)) loadTwo' = runErrorT $ do foo ← loadConfigFile' "foo" bar ← loadConfigFile' "bar" return (foo, bar)This is much closer to what you're trying to express. The monad is letting you say: "first load the file foo, and stop here and return if there's an error. then, ...".
All that was background. Here's the trick.
I had to change the type of
loadConfigFileso it would be fed into
runErrorT. That was ok in the above example, maybe, because the code ended up clearer. But what about code that you don't control, that has the old type with an
Eithernested inside an
IO? Simple: the
ErrorTconstructor exactly converts
IO (Either String a)into
ErrorT String IO a. So I could've used the original
loadConfigFileand just written the second function like this:
loadTwo'' :: IO (Either String (Config, Config)) loadTwo'' = runErrorT $ do foo ← ErrorT $ loadConfigFile "foo" bar ← ErrorT $ loadConfigFile "bar" return (foo, bar)
In summary, the basic pattern is:
- In circumstances where it makes sense, you can leave functions out of the
- Then, when combining them, wrap plain IO calls with
IO (Either ...)calls with
ErrorT, and the whole block in
Final trick: you can even use non-IO-using plain
Eithers here, too. Within
num <- ErrorT $ return $ parseRomanNumeral "iix"The
IO (Either ...), then the
ErrorTconverts it into an
Whenever code gets this hairy, though, the real consideration is that you're composing layers at the wrong abstractions and you should restructure it.