Hacker News new | past | comments | ask | show | jobs | submit login
Failing in Haskell (jappie.me)
123 points by zeepthee on Feb 26, 2022 | hide | past | favorite | 49 comments



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


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.


Haskell has had non-empty lists as a type for a long time: https://hackage.haskell.org/package/base-4.16.0.0/docs/Data-...

Having partial functions in the Prelude is, as far as I know, widely regarded as a mistake and they are only kept around for backwards compatibility. Anyone writing code nowadays should be using safeHead or non-empty lists.


Or pattern matching that takes account of the empty list case. Orrrrr using a fold! I usually find when I start matching on list values that the function could be better expressed with a fold instead.


Sure, but non-empty lists don't generalise easily to the case where you need the nth element, unless you nest them awkwardly.


> But I also admire the patience of anyone who actually manages to use it in practice

Nothing you point out gets even close, in my mind, to stuff like null pointers or untyped code. So I wonder what languages you have in mind that require less patience.

> you need IO for that. Which the language makes intentionally hard to use

Well, that is simply not true.


>Nothing you point out gets even close, in my mind, to stuff like null pointers or untyped code. So I wonder what languages you have in mind that require less patience.

You two are talking about two different things: - The parent is talking about the ecosystem, how menial tasks have tooling in "less interesting" languages - You are talking about the language itself

I would venture to guess that the parent would agree with you, if talking about the language in a vacuum.

An interesting competition would be to develop a complex product, without external dependencies.

My sad guess is that languages that are filled with escape hatches, like Java, Javascript, or python, would defeat more strict languages.

It's a sad guess, because I actually do prefer the Haskell way.


> Well, that is simply not true.

I think it's true, for example catch [1] requires IO. Do you mean with unsafePerformIO, or something else?

1: https://hackage.haskell.org/package/base-4.16.0.0/docs/Contr...


> But I also admire the patience of anyone who actually manages to use it in practice, If I'm cynical, I'd say that's because little practical stuff is often not worth publishing papers about.

I feel my impatience pushes me towards Haskell if anything... local-reasoning for instance rather than "understand this entire call chain" requires less patience and is easier to get right.

> there are so many little papercuts that don't get resolved, basically for a decade or more.

I've been using Haskell a decade in practice, can you tell me what papercuts you had in mind? I'm assuming I and other real world Haskellers might just see them as much less of a priority all things considered, but I'd like to be sure I'm not missing something.


In 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!


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


True, but I think enabling more cases where you can use function composition instead of pattern matching and explicit recursion would be a win.


This is why I see PureScript as a better starting point. It was modeled after Haskell, but since it was created in 2013, many of the design choices were to avoid these sorts of things.


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


There are some alternative Preludes that attempt to fix this, bringing a safer std lib to the table.


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 thing, but is the way to ensure a full Turing complete language where it is possible to define non-terminating behavior. Hence it really is by design.


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

This is a bit strange. It's like caring deeply about whether printf fails, but not so much whether array indexing is out of bounds. Haskell has a great story for both kinds of issue, and even its exceptions are better than panics IMO, even if they are about as tricky to use as POSIX signals, but it is relatively obscure and stigmatized to do a gross thing like use unsafePerformIO, but actually quite common/natural and accepted to use head. Lots and lots of people know to do the right thing for the latter, and there is something of a community push to avoid them, but it's just interesting to note how easy it is to make one mistake versus the other, when both matter a lot. One is treated as fundamental, and the other is not, but day to day, both kinds of issue lead to a similar magnitude of headaches, so the disparity is noteworthy.

I'd love it if even just the type signature recorded that exceptions are possible, even if there is no practical effect on how or where it is used.


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.


You make a good point about IO, I forgot how it's also not great about errors (but isn't there are an IO monad with better error treatment? -- it has been several years...). I also agree about granularity and tedium, but that's orthogonal to whether exceptions are the best way to approach such errors, and I don't think they are. Even Go's approach of explicit if-return is not tedium to me, but there are even less tedious approaches, that still let you handle the handle-able errors and do some last-ditch cleanup or just panic on the unhandle-able ones like indexing errors.

The interesting thing about Haskell exceptions are the async ones and the ability to `throwTo`, but I never really had a use for that, so on the whole, that was a bit of an encumbrance too. It's like trying to write exception safe C++ -- tedious and easy to get wrong. I remember a fair few sections of Parallel and Concurrent Programming in Haskell that temporarily didn't handle exceptions correctly, and it often wasn't for pure pedagogical reasons. Great book though.


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.


> quite common/natural and accepted to use head

No it isn't, not at all.

What absurd slander.


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


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


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


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


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


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


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


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


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


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


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


> The trifecta .. mathematical origins

Ends up reading Leibniz and converting to Catholicism.


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


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


Maybe some of the experts could name the haskell equivalent libraries/wrappers.


I'm certainly not an expert (have only dabbled in both Haskell and Python, and never used numpy), but a web search found https://pechersky.github.io/haskell-numpy-docs which compares numpy to https://hackage.haskell.org/package/hmatrix. I also came across https://hackage.haskell.org/package/vector.


The old joke comes to mind:'How do you recognize a guitar player in the audience of a concert? They stand in a corner look at the stage and say 'I also could do that' '


What Haskell did with Monads is nice, but eventually Monads are just tags on what functionality the function uses.

That being said, I like that Nim and Koka did exactly that. You just tag the functions (IO, Async, Whatever) and it works.

In Haskell, you need monad transformers (which have a runtime costs) or whatever else was made to allow you to work with multiple different effects.


> which have a runtime costs

As monad is just an interface, it doesn't necessary cause runtime costs. Identity is a monad too. Effects may not always require sacrificing performance, but as they can be used to implement exceptions they are not just free compile time annotations. Also the differences discussed there: https://www.reddit.com/r/haskell/comments/3nkv2a/why_dont_we...


Monad transformers are different from monads. Monad transformers do have runtime costs, they are adding indirection at runtime.


Sometimes - it's pretty cool what GHC can do


It's hard because there are so many concepts to understand. After reading one Python book you can write solid programs in Python. Not so in Haskell, you would need to understand also the extensions of the language which are popular and understand the best practices (what to use to compose I/O and in which context for example), on top of all the basics. That and understand how to work with complex types in libraries: that require time. That would be too much for one book.


> After reading one Python book you can write solid programs in Python.

Okay, so our goal is to write a solid program. Let's see...

> Not so in Haskell, you would need to understand also the extensions of the language which are popular

You can simply go with vanilla Haskell2010. Dealing with strings will be a bit cumbersome, dealing with records will be a bit cumbersome, but you are still at 50% the boilerplate of an average java codebase.

> and understand the best practices (what to use to compose I/O and in which context for example)

No! This is what I was aiming at: You don't have to understand these best practises to have a solid program. Throw everything into one massive do-Block and the resulting program will be at least as solid as the solid python program.

> That and understand how to work with complex types in libraries

I concur that Python documentation is heaps better than Haskell documentation, although we are slowly improving. That said, I think the work is not harder: Learning how to speak with a postgres database or do numerical tasks requires times, period. What is different to Python is that the time spent chasing runtime errors is spent chasing compile errors in Haskell.

Another user linked this comparison of numpy vs. the Haskell equivalent, hmatric. It does not look more complicated in my opinion: https://pechersky.github.io/haskell-numpy-docs/


> you would need to understand also the extensions of the language

Not really. Extensions typically remove restrictions rather than add features, or rather, the ones you are likely to want to use do.


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


This isn't isolated to Haskell. Bad errors are everywhere. And by some of my colleagues, I mean YOU TOO!


I changed it, lov me HL33tibCe7 senpai




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: