
On Sat, 16 Feb 2008, Alan Carter wrote:
I'm a Haskell newbie, and this post began as a scream for help.
Extremely understandable - to be blunt, I don't really feel that Haskell is ready as a general-purpose production environment unless users are willing to invest considerably more than usual. Not only is it not as "batteries included" as one might like, sometimes it's necessary to build your own batteries! It's also sometimes hard to tell who the experts are, especially as many of us mostly work in fairly restricted areas - often way away from any IO, which is often a source of woe but whose avoidance leaves something of a hole in some coders' expertise. The current state of error-handling is something of a mess, and there are at least two good reasons for this: * Errors originating in the IO monad have a significantly different nature to those generated by pure code * We don't have[1] extensible variants, leading to the kinds of problem you complain about with scalability as the number of potential errors increases It's been a while since I was in the tutorial market, but I don't think many tutorials address the first point properly and it's a biggie. Most IO functions are written to throw exceptions in the IO monad if they fail, which forces you to handle them as such. So, here's an example: import System.IO fileName = "foo.bar" main = (do h <- openFile fileName ReadMode catch (hGetContents h >>= putStr) (\e -> do putStrLn "Error reading file" hClose h ) ) `catch` (\e -> putStrLn "Error opening file") On my machine, putting this through runhaskell results in a line "Error opening file", as unsurprisingly there's no foo.bar. Producing an error opening is harder work, whereas if I change filename to the program's source I get the appropriate output. It may say something about me that I didn't get this to compile first time - the culprit being a layout error, followed by having got the parms to openFile in the wrong order. Caveats so far: there are such things as non-IO exceptions in the IO monad, and catching them requires Control.Error.catch, which thankfully also catches the IO exceptions. If putStr were to throw an exception, I'd need yet another catch statement to distinguish it (though it'd be handled as-is). The sensible thing though is probably to use Control.Error.bracket (which is written in terms of catch) thusly: import System.IO import Control.Error filename = "foo.bar" main = bracket (openFile filename ReadMode) (\h -> hGetContents h >>= putStr) (\h -> hClose h) So from here, we have two remaining problems: 1) What about pure errors? 2) What about new exception types? I'll attack the second first, as there's a standard solution for IO and a similar approach can be adopted in pure code. It's a fairly simple, if arguably unprincipled, solution - use dynamic typing! Control.Error offers us throwDyn and catchDyn, which take advantage of facilities in Data.Dynamic. Pure code can make use of Data.Dynamic in a similar manner if needed. Personally I'm not too happy with this as a solution in most cases, but it's no worse than almost every other language ever - I guess Haskell's capabilities elsewhere have spoiled me. As for pure errors, there're essentially two steps: 1) Find a type that'll encode both the errors and the success cases (Maybe and Either are in common use) 2) Write the appropriate logic I'll not go into step 1 much, most of the time you want to stick with Maybe or Either (there's a punning mnemonic that "if it's Left it can't have gone right" - it's usual to use Right for success and Left for failure). The second point is where you get to adopt any approach from writing out all the case analysis longhand to using a monad or monad transformer based around your error type. It's worth being aware of Control.Monad.Error at this point, though personally I find it a little irritating to work with. By the time you're building customised monads, you're into architecture land - you're constructing an environment for code to run in and defining how that code interfaces with the rest of the world, it's perhaps the closest thing Haskellers have to layers in OO design. If you find you're using multiple monads (I ended up with three in a 300 line lambda calculus interpreter, for example - Parsec, IO and a custom-built evaluation monad) then getting things right at the boundaries is important - if you've got that right and the monad's been well chosen then everything else should come easily. Thankfully, with a little practice it becomes possible to keep your code factored in such a manner that it's easy to refactor your way around the occasional snarl-ups that happen when a new change warrants re-architecting. That or someone just won buzzword bingo, anyway. Anyway, I hope this's been helpful. [1] There are ways of implementing them in GHC, but in practice they're not used enough for anyone to be comfortable building an error-handling library around them -- flippa@flippac.org Society does not owe people jobs. Society owes it to itself to find people jobs.