evan_tech

Previous Entry Share Next Entry
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 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:
  1. In circumstances where it makes sense, you can leave functions out of the ErrorT monad.
  2. Then, when combining them, wrap plain IO calls with liftIO, the IO (Either ...) calls with ErrorT, and the whole block in runErrorT.

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.