08:17 am, 27 Mar 08
haskell trick #1: using ErrorT
Here's a Haskell trick that took me a while to figure out but that I use all the time.
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
Ok, more background: you'll often mix this with IO. For example:
All that was background. Here's the trick.
I had to change the type of
In summary, the basic pattern is:
Final trick: you can even use non-IO-using plain
Whenever code gets this hairy, though, the real consideration is that you're composing layers at the wrong abstractions and you should restructure it.
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
String as my Error type and Either as my MonadError instance. 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
addRoman is Left with 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 this
It'd be nice to again compose these like I did in addRoman. That's what the ErrorT monad 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
loadConfigFile so 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 Either nested inside an IO? Simple: the ErrorT constructor exactly converts IO (Either String a) into ErrorT String IO a. So I could've used the original loadConfigFile and 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
ErrorTmonad. - Then, when combining them, wrap plain IO calls with
liftIO, theIO (Either ...)calls withErrorT, and the whole block inrunErrorT.
Final trick: you can even use non-IO-using plain
Eithers here, too. Within loadTwo'':num <- ErrorT $ return $ parseRomanNumeral "iix"The
return brings the Either into IO (Either ...), then the ErrorT converts it into an ErrorT.Whenever code gets this hairy, though, the real consideration is that you're composing layers at the wrong abstractions and you should restructure it.