[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