There'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.
It'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]. Given a new language like Mars, he could provide a fix, but we have too much baggage to break Haskell's implementation.
> 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.
Yeah. 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.
What 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.
And there is a billion ways how to construct an empty list. You can't just grep for '[]'. It can be hidden inside of a 'catMaybes', 'tail', or any other function which returns a list and makes no guarantees about it's size, of which there are plenty.
So first of all, head as it is in Prelude is a partial function. The error isn't a null pointer exception, it is an issue with head not being defined on empty lists.
I personally would never use head in code I write. Instead I'd use a version which looks like [a] -> Maybe a which does properly solve the null problem.
On top of that, the compiler can warn/error if partial functions are used. I do this for all of my projects.
There has been a lot of debate over these functions should even existing in Prelude. I think they are a blight on the language.
For example, in the `head []` case, ghc has an option to locate the exact source of the error:
-xc
(Only available when the program is compiled for profiling.) When an exception is raised in the program, this option causes a stack trace to be dumped to stderr.
This can be particularly useful for debugging: if your program is complaining about a head [] error and you haven't got a clue which bit of code is causing it, compiling with -prof -fprof-auto and running with +RTS -xc -RTS will tell you exactly the call stack at the point the error was raised.
The output contains one report for each exception raised in the program (the program might raise and catch several exceptions during its execution), where each report looks something like this:
*** Exception raised (reporting due to +RTS -xc), stack
trace:
GHC.List.CAF
--> evaluated by: Main.polynomial.table_search,
called from Main.polynomial.theta_index,
called from Main.polynomial,
called from Main.zonal_pressure,
called from Main.make_pressure.p,
called from Main.make_pressure,
called from Main.compute_initial_state.p,
called from Main.compute_initial_state,
called from Main.CAF
...
There can be unexpected crashes in Haskell, but not because of null.