4 ms·
> How often do programs crash because of an unexpected null value? Haskell programs never do! > head [] error "Prelude.head: empty list" Ok, so you do
by sparkie 12y ago
> How often do programs crash because of an unexpected null value? Haskell programs never do!
> head []
error "Prelude.head: empty list"
Ok, so you don't call it `null`, but it still crashes the program. Its deceptive to claim null isn't present because it's conventional to avoid using "error" in favor of Maybe, but it's still there, and used throughout Prelude. The REPL on the site hides these errors too, which is going to be pretty confusing to a newcomer who takes the head of the empty list and gets back "null" (the absence of a value) of type a.
> (head x) + (head (head x))
:: Num a => a
Hiding errors makes it hard for a beginner to see what went wrong, although perhaps not in this trivial example.
- wtetzner 12y agoThere's a difference between crashing and returning null. If head returned null on an empty list, that null could be passed around to different places in the program before it crashed from a null pointer exception. There can be unexpected crashes in Haskell, but not because of null.
- sparkie 12y agoIt's not equivalent to null, but analagous to it. With lazy evaluation, I can pass also around the expression `head []` to different places, and it won't complain until I attempt to evaluate it - much like a language with null won't complain until you try to dereference it. Sure there's a difference though - you can see immediately where the problem is with Haskell's error, but it's more difficult to trace why a value might be null in imperative code (Which I think has more to do with mutability than the presence of null). I was also highlighting that the tryhaskell REPL actually behaves like null, because it doesn't terminate immediately as you would expect. Just yesterday I was discussing this topic with the author of the Mars language, because his record implementation is equivalent to Haskell's and is flawed in the same way (sum types + records leading to what is effectively the equivalent of `null`) [https://news.ycombinator.com/item?id=8005116 https://news.ycombinator.com/item?id=8005116]. Given a new language like Mars, he could provide a fix, but we have too much baggage to break Haskell's implementation.
- wtetzner 12y ago> With lazy evaluation, I can pass also around the expression `head []` to different places, and it won't complain until I attempt to evaluate it - much like a language with null won't complain until you try to dereference it. It won't complain until you try to evaluate it temporally, but the stack trace will point to the call to head.
- rtfeldman 12y agoYeah. A major reason null is called the "billion dollar mistake" is that once you finally do get a NullPointerException (or the like), it can take a huge amount of time to track down where the null value originated. If you take the head of an empty list in Haskell, you get an exception right away. Not a poisoning of the well, like you do in so many other languages. That's absolutely a benefit of Haskell worth touting.
- T-hawk 12y agoThat's still the same problem, isn't it? You still have to track down where the empty list originated, just as the null value in C. I'm not a Haskeller, but do the language and tools make that easier than in other ecosystems?
- deleted 12y ago[deleted]
- freyrs3 12y agoThe difference is the program halts immediately at the place in the program where the semantics break down. Unlike in C where you can program with nulls and pass them around, potentially smearing the problem around the entire program.
- sparkie 12y agoWhat makes it easier is immutability - if given an empty list, the only place that could've made this list empty is the place it was constructed - because there's no way some other function can come and delete items from it. A function which "removes" items from a list doesn't actually do such thing - it creates a brand new one and adds all the same elements except the items you requested being removed. In this way, there's only one possible path that the list could've come from - through the pure functions which use it - until head is reached. Given that each function is referentially transparent, applying the same input list to a function in the debugger will always produce the same result, so it's simple to call a function with some sample data in ghci, and the result you get will be the same result you get in the compiled program. Debugging/tracing is perhaps more difficult than with tooling you might already be familiar with - but it's much more rare that you need to even use them, because it's obvious what values a function should return - they don't have any state which could influence otherwise.
- adrusi 12y agoThere are a couple of Prelude functions that are broken, most notably `head` and `tail`. The problem is that they use non-exhaustive pattern matching in their definition: head :: [a] -> a head a:_ = a tail :: [a] -> [a] head _:as = as Fortunately, use of these functions is unidiomatic, not just because they're one of the only ways that haskell programs can crash, but because they're less clear than the alternative, pattern matching. The haskell compiler will give you a warning by default if you write a function that performs a non-exhaustive match. tl;dr: head and tail are mistakes, but they're never used.
- sparkie 12y agoI picked head and tail as primary examples, but it's not limited to them. As I discussed in the link I've posted to a discussion yesterday, one can easily create their own incomplete pattern matching by mistake, without the compiler complaining, by using records. eg: data List a = Nil | Cons { head :: a, tail :: List a } I encounter this misfeature of Haskell frequently - records and sum types don't mix, because they lead to an incomplete pattern match in the record fields. Yes, it's unidiomatic Haskell, but a beginner does not know this, and just because it's conventional to avoid using it, does not mean it isn't still there - anyone could make this mistake.
- deleted 12y ago[deleted]