9 ms·
Failing in Haskell
- sharmin123 5y ago[dead]
- danidiaz 5y ago> If we need to compose these errors in a larger program we can simply wrap previous errors in a bigger sumtype This approach is being adopted in GHC itself to compose errors happening at different stages of the compilation pipeline: each stage has its own error type which later becomes a branch of the global error type. Another interesting post about errors-as-values in Haskell is "The Trouble with Typed Errors": https://www.parsonsmatt.org/2018/11/03/trouble_with_typed_errors.html https://www.parsonsmatt.org/2018/11/03/trouble_with_typed_er...
- codeflo 5y agoThe GHC approach you describe is also what people do with Results in Rust, and (analogously) with Java's typed exceptions. The idea is that in a multi-layered program, every layer exposes errors that are semantically appropriate for that layer. So (to pick a silly little example) a database call would expose a DatabaseError, not a raw network error if the connection is interrupted. And so on until you get to the level of application-level errors. I think that can work very well. In the same spirit, I find the article you linked to a bit silly, at least the examples they picked. Following the logic above, there shouldn't even be a "HeadError" exposed anywhere up the call chain. Inventing a complicated mechanism to propagate the error upwards is the opposite of what you want to do; you want elegant ways to handle the problem locally. Having a special singleton HeadError isn't wrong, but I think "Maybe a" would also be a perfectly fine return value for head (as I mentioned in a sibling post, that's what Rust does): head can only "fail" if the list is empty, so there is no actual information in the "error" value.
- fn-mote 5y ago> so there is no actual information in the "error" value. At least as a beginner, the information about which line the error occurred on would be helpful.
- agentultra 5y agoThe ‘head’ function is an unfortunate historical artifact and not the norm these days. In practice there are libraries that expose a head function that returns a value… but better still, well typed programs can avoid the need for it altogether: there are non-empty lists to consider in which head is trivially safe to use, provided one can construct such a value. One error handling strategy not often employed is to prefer code that is correct by construction. It can’t always be done but it’s nice when you can do it. update spelling
- ParetoOptimal 5y ago> Another interesting post about errors-as-values in Haskell is "The Trouble with Typed Errors": https://www.parsonsmatt.org/2018/11/03/trouble_with_typed_er https://www.parsonsmatt.org/2018/11/03/trouble_with_typed_er... At the point of `AllErrorsEver` I usually find throwing an exception make sense. That doesn't negate the use of defaulting to `Either` rather than exceptions for the "leaves" of your tree of code where each defines a sum type of errors at the function or maybe the module level. Edit: My last recommendation is basically consistent with the article.
- default-kramer 5y ago> https://www.parsonsmatt.org/2018/11/03/trouble_with_typed_errors.html https://www.parsonsmatt.org/2018/11/03/trouble_with_typed_er... Nice. I've asked for a way to do that in the past and never found a good answer, in any language! It's not exactly conventional Haskell though, is it? What I really want is first-class support in the language - something like checked and unchecked exceptions in Java, except that if a method declaration lacks a `throws` keyword then all the checked exceptions are inferred by the compiler. For example, the compiler might add `throws A, B, C` to a method that lacks a `throws` keyword. Now if you want to assert that a certain method throws a certain exception, you could write `throws A, *` which means "If this method does not throw an exception of type A, I want a compiler error. If this method throws additional exception types, infer them as usual." Omitting the asterisk (eg `throws A`) would disable the inference and thus would work like a normal `throws` in real Java. You should also be able to assert that a certain exception type is not thrown, for example `throws * except F, G` or something like that.
- the_duke 5y agoI only used Haskell for small projects. I admire the language, but I found error handling to be one of the weakest and most inconsistent elements. To the point of being annoying and time consuming. Several popular libraries I used threw exceptions for expected failures (like a non 2xx HTTP response) and required wrapping. Even the standard prelude is full of partial functions. (head...). I saw a wild mix of Either, exceptions and custom monads all over the ecosystem. So if you want to have a coherent strategy you end up doing a lot of error juggling. Manual errors with Either can make it very hard to figure out where an error came from because they don't capture backtraces. So if you don't have a very specific error for each failure point you are left guessing and debugging.
- maweki 5y ago> they generally don't capture backtraces Backtraces with higher-order functions, lazyness, partial applications, and all the transformations going on (SKI, CPS, or whatever the GHC does), I don't think any kind of backtrace would be legible.
- xyzzyz 5y agoThe transformations usually can and should be implemented in a way that preserves the original call stack information. However, you are right that laziness makes backtraces less useful: they still are correct, but they pop up in completely unexpected moment. For example, you do something like “let x = f y in return (g x)”, where x is a (lazy) list, and g :: IO [U] -> V for some types U and V. Then somewhere deep into g’s callstack, 123th element is accessed, which forces its computation, which results in exception. You then get an error, and backtrace should naturally come from function f, but in fact it actually happened while executing g, and if g is missing from the trace, a natural intuition from strict languages would suggest that error happened before execution entered g, because f is called before g, which gets its return value.
- b123400 5y agoI have to echo your point on inconsistency. Our company uses Haskell and the Haskell team love to define their own solutions which make things even more inconstant. For error handling they end up using an extensible type-level-list containing possible error types, embedded in an extensible effect monad. We also have list, array, vector, and our own collection types in the same place. It feels like everyone want to make things better by using/making something new, instead of making them consistent.
- codeflo 5y agoI've written small stuff in Haskell a decade ago. I have a soft spot for the language -- it has clearly influenced many notable languages that came after it. But I also admire the patience of anyone who actually manages to use it in practice, there are so many little papercuts that don't get resolved, basically for a decade or more. If I'm cynical, I'd say that's because little practical stuff is often not worth publishing papers about. Error handling was, for me, a big one. For a functional language, Haskell seems very obsessed with exceptions. Even supposedly pure stuff, like "head" (first element of a list) throws an exception if the list is empty. You'd think Haskell would be the first language to have it return a Maybe value, but no. (Rust, BTW, gets functions like this right; they all return an Option.) This reliance on exceptions clashes hard with the functional paradigm. Exceptions are "magic": They are special additional values that any type can have (so an Int can either be an actual integer or an exception value), but you can't test for them or handle them in any way pure code, you need IO for that. Which the language makes intentionally hard to use, that's Haskell's entire thing.
- exdsq 5y agoIn Haskell you’d use pattern matching guards for the empty list which works better for recursion & doesn’t require you to handle the Maybe monad in primitive data structures. Even though Monads were introduced to programming after Haskell had been written (to deal with IO, SPJ and Wadler have a good paper on this) I don’t know if this would have been worth changing. After all, you can always wrap a custom Maybe<List> if you need it!
- codeflo 5y agoTrue, but I think enabling more cases where you can use function composition instead of pattern matching and explicit recursion would be a win.
- dundarious 5y agoOP is saying the existence of head means someone will use it and get the paper cut. It's true you just shouldn't use it (even when you know it's non-empty, write the throw yourself). But that's why it's an annoyance. Arguably it's even more of a problem, it's a "foot gun". It would be nice if the Prelude was just replaced, but that obviously presents a host of annoying challenges. Several alternative Preludes exist, but none appear to be becoming the new center of mass.
- schwurb 5y agoAdressing the whitespread conception "It is hard to programm in Haskell because it is pure": If you can write python, you can write Haskell. Don't believe me? 1. Write your program completely in the IO Monad, in a huge do-block 2. Factor out as much pure functionality as possible (= Have as little code in your big IO-programm as possible.) Start at 1. and iterate 2. as many times as you please. It will already be a program that prevents many traps that would bite you in other langauges. Haskell knows exactly whether you are looping over an array of strings or an array of chars. (Why all the buzz about pureness, effects and so on? Well, with Haskell you can design with a high granularity and reliability what sideeffect is caused where. But you are not forced to use that feature.) Other tipps: - Build small projects. - Read as few tutorials on monads as possible. You might even get by with 0. - The trifecta of Haskell typeclasses are the functor, applicative, monad. I would advise you to not try to understand their mathematical origins, but just look up how the are used. They will crop up naturally when you build even small projects and then they will make sense.
- zeepthee 5y ago> The trifecta .. mathematical origins Ends up reading Leibniz and converting to Catholicism.
- andi999 5y agoI like the idea of iterating from imperative to functional. Here the devils advocate for your if you can do it in python you can do it in haskell: I use quite a bit of numpy, scipy and matplotlib, are there equivalent libraries for Haskell?
- AnimalMuppet 5y agoWell... wasn't numpy, at least initially, a Python wrapper around Fortran libraries? Sure, that made them accessible to a bunch more people, but it wasn't some Python-only wonder. Someone could probably write the same bindings for Haskell, if they haven't already.
- andi999 5y ago
- HL33tibCe7 5y ago> Some of my intelligent colleagues mucked up error handling. Not only were they failing, they were failing WRONG 1. This frustrates me because doing failing correctly in Haskell is quite easy Leaving aside that publicly shitting on your colleagues is an extremely bad look and makes you come across incredibly arrogant, isn’t the fact that the intelligent colleagues didn’t get it pretty strong evidence that error handling in Haskell in fact isn’t easy?