Live data from Hacker News

Failing in Haskell

jappie.me

21–30 of 55 posts

Re: Failing in Haskell

#21
post #11
post #4

I 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, e…

> 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.

The 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.

Re: Failing in Haskell

#22
post #4

I 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, e…

> 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.

Why wouldn't you have a very specific error for each failure point?

Funnily enough, I theoretically agree with your point but can't remember being bitten by it in practice for some reason.

Maybe you can help by giving an idea of a real world example of this?

Re: Failing in Haskell

#23
post #3

> 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": ht…

> 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...

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.

Re: Failing in Haskell

#24
post #20

Adressing 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 p…

> The trifecta .. mathematical origins

Ends up reading Leibniz and converting to Catholicism.

Re: Failing in Haskell

#25
post #5

I'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 publ…

No post body was provided.

Re: Failing in Haskell

#26
post #5

I'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 publ…

Partial functions and exceptions are a compromise solution for the fact that you sometimes do know more than the compiler does. I think it's fine to throw an exception in the case of "programmer error". It's the equivalent of assertions in other languages. Yes, it can blow up, but at least the error is a bit more localised.

Having head return a Maybe means that you'll have to awkwardly handly a Nothing case even in situations where there is no sane behaviour to be added because it just simply would make no sense for a particular list to be empty unless you've introduced a bug somewhere else. It's hard to "recover" from such an error.

The same goes for e.g. division, which is partial too (can't divide by 0), but having it return Maybe would make arithmetic incredibly awkward. You could instead define e.g. x/0=0 or any other value—some languages like Coq or Pony do that, but I think that has the drawback that this makes it rather easy to mask some ugly errors.

In many such cases, the Haskell type system (without advanced extensions) is not expressive enough to encode everything you know about your values. In a language with dependent types, such as Idris, you can specify the length of the list in your type; then you can have a type-safe, total head function that doesn't return Maybe. You can also write a division function that requires a proof (possibly implicit) that the denominator is not zero. But dependently typed languages are much more niche than Haskell.

Re: Failing in Haskell

#27

Earlier quoted context omitted.

What you are running into is the pureness of Haskell. The `head` function in Haskell is only partially defined. What you see as an exception is a case where a function is not defined. This is all by intent. defining a `head :: [a] -> Maybe a` is a very simple matter and definitely something a developer should be encouraged instead of using the prelude. exceptions in Haskell are not meant to be used as a first class t…

The issue as I see it, is that one of the main selling points of a pure language like Haskell, is that you have to explicitly state where a certain class of surprises/failures (from IO) lie, and therefore, you can account for them better, handle them cleanly, prevent them from arising accidentally or in some ways maliciously, etc. Partial functions are another kind of surprise/failure, but they are not at all explici…

Idris does that. If you add "%default total" to a file (or the equivalent compiler flag), it will make sure every function terminates unless it's annotated with "partial". In the best case, only your main function and a couple others need to be partial.

Re: Failing in Haskell

#28
post #5

I'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 publ…

> 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. I

Great summary of what it’s like to use any niche language. You don’t realize the value of a mature and highly used ecosystem until you have to chase issues in an ecosystem where maybe 5 other people total are doing the same thing you’re doing and nobody has updated some library you need for 3 years.

Fun for hobbies, terrible for real work.

Re: Failing in Haskell

#29
post #26
post #5

I'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 publ…

Partial functions and exceptions are a compromise solution for the fact that you sometimes do know more than the compiler does. I think it's fine to throw an exception in the case of "programmer error". It's the equivalent of assertions in other languages. Yes, it can blow up, but at least the error is a bit more localised. Having head return a Maybe means that you'll have to awkwardly handly a Nothing case even in s…

[deleted]

Re: Failing in Haskell

#30

Earlier quoted context omitted.

What you are running into is the pureness of Haskell. The `head` function in Haskell is only partially defined. What you see as an exception is a case where a function is not defined. This is all by intent. defining a `head :: [a] -> Maybe a` is a very simple matter and definitely something a developer should be encouraged instead of using the prelude. exceptions in Haskell are not meant to be used as a first class t…

The issue as I see it, is that one of the main selling points of a pure language like Haskell, is that you have to explicitly state where a certain class of surprises/failures (from IO) lie, and therefore, you can account for them better, handle them cleanly, prevent them from arising accidentally or in some ways maliciously, etc. Partial functions are another kind of surprise/failure, but they are not at all explici…

IO is not (primarily) about where failures lie, but about where side effects lie- side effects are where you start caring about the order of execution.

Array indexing failures, on the other hand, are not something you typically care about at quite that granularity- they're usually just bugs, not something to recover from except perhaps at a much higher level.

The parent comment lumps these kinds of failures in with non-termination, which in pure functions is also typically just a bug rather than a recoverable failure. And this one isn't something you can generally check for, either- with lazy evaluation, every type in a Haskell program by default includes a "bottom" value.

I think both choices were made for a similar reason- actually handling array bounds check failures everywhere is pointless tedium (and often better folded into the iteration itself), and actually handling possible non-termination by using a total language can also get pretty tedious. There are languages that do both, and they have their uses, but Haskell went a different direction.

Post reply on HN