[Haskell-cafe] Doubting Haskell

Philippa Cowderoy flippa at flippac.org
Sat Feb 16 18:46:48 EST 2008


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 at flippac.org

Society does not owe people jobs.
Society owes it to itself to find people jobs.


More information about the Haskell-Cafe mailing list