So coming from that type of language (where you can't segfault) I'd definitely call a panic a "crash" simply because it's the analog of an unhandled exception, which I always called a crash.
So this terminology probably varies between ecosystems
It's a concept implemented in eg Erlang. Crash in their sense means basically, 'kill -9'.
Haskell is a great example of how handling errors can be harmful, since it violates confluence, even though throwing errors is fine!
Confluence is the property that evaluation order doesn't change the meaning of a program, e.g. we can do:
(1 + 2) * (3 + 4)
3 * (3 + 4)
3 * 7
21
Or: (1 + 2) * (3 + 4)
(1 + 2) * 7
3 * 7
21
We could inline some function calls if we like, thanks to referential transparency; we can even evaluate "under a lambda" (i.e. evaluate the body of a function before calling it, or evaluating the branches of an `if/then/else` before picking one); regardless of which way we evaluate, if we reach an answer (i.e. don't get stuck in a loop) then it will be the same answer: (1 + 2) * (3 + 4)
(1 + 2) * (if 3 == 0 then 4 else pred 3 + inc 4)
(1 + 2) * (if 3 == 0 then 4 else pred 3 + 5)
(1 + 2) * (if 3 == 0 then 4 else 2 + 5)
(1 + 2) * (if 3 == 0 then 4 else 7)
(1 + 2) * (if False then 4 else 7)
if (1 + 2) == 0 then 0 else (if False then 4 else 7) + (pred (1 + 2) * (if False then 4 else 7))
if (1 + 2) == 0 then 0 else 7 + (pred (1 + 2) * (if False then 4 else 7))
if 3 == 0 then 0 else 7 + (pred (1 + 2) * (if False then 4 else 7))
if 3 == 0 then 0 else 7 + (pred (1 + 2) * 7)
if False then 0 else 7 + (pred (1 + 2) * 7)
if False then 0 else 7 + (pred 3 * 7)
7 + (pred 3 * 7)
7 + (2 * 7)
7 + 14
21
Exception handlers break this, since we can write expressions like: try (head [42, Exception1, Exception2])
catch Exception1 -> 1
Exception2 -> 2
We can evaluate this one way: try (head [42, throw Exception1, throw Exception2])
catch Exception1 -> 1
Exception2 -> 2
try 42
catch Exception1 -> 1
Exception2 -> 2
42
Or another way: try (head [42, throw Exception1, throw Exception2])
catch Exception1 -> 1
Exception2 -> 2
try throw Exception1
catch Exception1 -> 1
Exception2 -> 2
1
Or another way: try (head [42, throw Exception1, throw Exception2])
catch Exception1 -> 1
Exception2 -> 2
try throw Exception2
catch Exception1 -> 1
Exception2 -> 2
2
This gives 3 different answers. Note that the throwing itself doesn't cause this problem, because we treat an "unhandled exception" as not getting an answer (equivalent to an infinite loop).When I first grokked this it was quite enlightening: adding features to a language can make it less useful. It's not that certain features (like throwing or catching exceptions) are "good" or "bad", but that we must think of languages as a whole, and knowing that some things aren't possible (like observing evaluation order) can be just as useful as allowing more things. This contrasts strongly with the tendency of languages to accumulate features over time, especially when the major justification is often "we should have it because they do" :)
There's also a nice discussion on errors vs exceptions at https://wiki.haskell.org/Error_vs._Exception
head [0, throw E]
also produces such an "indeterminate" answer depending on order of evaluation, as long as you define things as you have here.The solution in a lazy language like Haskell is to be specific about when things get forced, which outside of explicit overrides only happens in response to actual usage of that expression, eg. when the value is printed. At this point you're naturally forced to introduce either sequentialization or explicit parallelism; in the former there is no issue and in the latter you still need to explicitly sequence the results, of which the exceptions are a relevant part.
In a strict language like Rust, of course, you never aimed to have this property anyway.
> head [0, throw E]
> also produces such an "indeterminate" answer depending on order of evaluation, as long as you define things as you have here.
I disagree; note that I said:
> we treat an "unhandled exception" as not getting an answer (equivalent to an infinite loop)
More formally, throwing an exception/error results in _|_ (bottom) and all _|_ results are equivalent i.e. we can't tell "which error" we got. In particular, we can't tell the difference between _|_ due to an error being thrown, and _|_ due to an infinite loop.
This is important in a general recursive language, since we can't know (in general) whether evaluating some value (with whatever strategy) will eventually produce a result or not. Consider the following, where omega = (\x -> x x)(\x -> x x):
head [0, omega]
Under a call-by-name strategy this will reduce to 0, under call-by-value it will loop forever, i.e. giving _|_. Should this count as breaking confluence?Total languages like Agda and Coq would say this breaks confluence, due to general recursion being a side-effect, and hence it should be encoded as such rather than allowed willy-nilly.
Turing-complete languages like Haskell would say that such encoding is cumbersome, and hence that their notion of confluence should be weaker; specifically, evaluating an expression using any evaluation strategy to get a value other than _|_ will produce the same value.
It just so happens that lazy evaluation has the property that if some (perhaps unknown) strategy can reduce an expression to normal form, then non-strict evaluation can also reduce it to normal form. In other words, lazy evaluation avoids all 'avoidable' infinite loops, but it still gets stuck in 'unavoidable' ones. That's nice, but isn't important for confluence.
You're perfectly right that introducing sequencing like `seq` throws a spanner in the works :)
My point is that they're not when you have `catch`, and that this distinction adds nothing that ⊥ doesn't already add with regards to order of evaluation.