3 ms·
>> 27 different ways to handle errors > Could you elaborate? I understand this is hyperbole, but I feel like the number in actuality is <= 3 Not quite 27, but
by gizmo686 8y ago
>> 27 different ways to handle errors
> Could you elaborate? I understand this is hyperbole, but I feel like the number in actuality is <= 3
Not quite 27, but exceptions in Haskell are quite messy. You have:
1) Exceptions. Which can be thrown from pure code, but only caught in IO. And which get thrown when evaluated, which causes a mess with Haskell's lazy evaluation. So the following code is not safe:
val <- catch (willThrowError) handler
print val
Because willThrowError may return a thunk that will not throw an error until you attempt to resolve it to a value. This means that it will not actually throw an error until `print val`, at which point it is outside of the catch, so your entire function will error, and your handler will not be called. To be safe, you want to do something like:
val <- catch (evaluate . force <$> willThrowError) handler
print val
Which requires the return value of willThrowError to be a support type. With the proper language extensions, it is not difficult to autogenerate the instance needed for a type to work with force, so long as you control the type definition and all the types it relies on supports it. I avoid actual Exceptions like the plauge, so I might be off on some details here.
2) Maybe/MaybeT/Either/EitherT
Arguably 4 methods, but I will concede that they are the same-ish.
3) ExceptT.
Maybe belongs in catagory 2?
4) MonadError
5) MonadFail
I might be missing somne.
- hardwaresofton 8y agoHey thanks for clarifying -- Maybe I've just been a victim too long, but those actually don't seem like distinct ways. As far as why you can throw an exception in pure code but only catch it in monad, much ink has been spilled[0]. I won't claim to completely understand the reasoning in that SO post and the linked paper[1], but the reasons for this dichotomy seem to be clear. 2) -> 5) are not ways of handling errors though, they're ways of characterizing/combining monadic computations that CAN error, IMO. In the end, it's still the errors-as-values (`Either SomeException value`) concept, but in combined with the fact that your computation is in a monadic context. Also, maybe I've just been brainwashed by myself to think that way after doing Haskell for this short amount of time. In practice, how I throw an error from a monadic computation (I generally have never done it from pure code) has been pretty well-defined, I haven't had to go down this particular rabit hole much at all -- ExceptT is what I've used the most (I've done most of my work with Servant[2]). Honestly what I've had problems the most with from my haskell code was dealing with the database or mail servers and tcp sockets and stuff -- stuff that failed at runtime because I put in some bad code Haskell couldn't check before runtime. That said, monads & monad transformers are indeed a complex concept, and are very confusing to beginners (I'm still not 100% confident in my knowledge of them), so this is definitely a point against Haskell. And of course your earlier point about unsafe functions in the std lib is as valid as ever. [0]: https://stackoverflow.com/questions/12335245/why-is-catching-an-exception-non-pure-but-throwing-an-exception-is-pure#12345665 https://stackoverflow.com/questions/12335245/why-is-catching... [1]: https://www.microsoft.com/en-us/research/publication/tackling-awkward-squad-monadic-inputoutput-concurrency-exceptions-foreign-language-calls-haskell/?from=http%3A%2F%2Fresearch.microsoft.com%2Fen-us%2Fum%2Fpeople%2Fsimonpj%2Fpapers%2Fmarktoberdorf%2F https://www.microsoft.com/en-us/research/publication/tacklin... [2]: https://haskell-servant.readthedocs.io/ https://haskell-servant.readthedocs.io/
- gizmo686 8y agoMy only real complaint about exception handling in Haskell is the interaction with laziness. 90% of my problems would go away if they just made the default catch functions include `evaluate . force` and save the lazy behavior for special "I know what I'm doing" functions. Without that, exceptions are just a loaded gun. Even with making exceptions "safe", I still much prefer the error-as-values approach. Otherwise, every pure function is really some type of ExceptT IO monad. I'm sure this escape hatch is useful to someone, but actually using it is a pretty big code smell.
- hardwaresofton 8y agoYeah this is actually an oft-repeated problem. I haven't really run into problems with enforcing strictness but that just means I haven't written/profiled enough Haskell yet, probably. Note though that there is the new Strict pragma: https://ghc.haskell.org/trac/ghc/wiki/StrictPragma https://ghc.haskell.org/trac/ghc/wiki/StrictPragma Also, this isn't really a solution to the problem (at best I'm just being an apologist), but you could write rules with hlint or any other code-smell-checking-tool to make sure no one every tries to use the escape hatch(es) you don't like.
- danharaj 8y agoWe basically ban throwing exceptions in pure code at my shop; usually we use ExceptT or throwIO or something. I used to think the multiplicity of error handling constructs in Haskell was confused and bad, but they all have their uses. The key insight is that your error handling should conform to the local structure of your code, not the other way around. Propagating errors across multiple component boundaries or multiple layers can be cumbersome but I think that reveals fundamental architectural weaknesses in the program itself.
- Silhouette 8y agoThanks, that covers much of what I had in mind before. Error handling in Haskell seems to suffer from two fundamental problems. One is that there is a clash between non-strict evaluation and exceptions, as your first example illustrates. I understand the arguments about why throwing can be done in pure code but catching needs to be in IO in Haskell, but those reasons are consequence of other design decisions in Haskell. The asymmetry is awkward, and it feels like you can never quite trust pure functions to do what you expect. It's a little like the long-standing criticisms of exceptions in C++, except that at least in C++ idioms have evolved to compensate, whereas shoving everything behind one or another of the `catch` variants in Haskell feels very unidiomatic to me. The other fundamental problem is that the different techniques for representing potential failure don't compose very elegantly. Given that flexible composition is arguably the biggest selling point of being lazy in the first place, that is unfortunate. The elegance gained from a language like Haskell in expressing tidy, pure code loses its shine a little if you then have to wrap a lot of real world code that calls out to other libraries to do things inside 17 levels of monad transformers, forced evaluations and catches. Of course I'm not suggesting that these are new or insurmountable problems or that no-one can write reasonably safe code in Haskell or anything silly like that. But I do think some advocacy, as if the type system is borderline magical, can be rather idealistic, and sometimes overlooks the warts that also arise as a direct result of the decisions that made the language safer and more elegant in other respects.