This isn't (just) about exceptions that escape; you also need to guarantee that fst (0, ⊥) returns 0 rather than ⊥. Heck, you're practically required to do the same for
fst (0, [0..(10 ^ 10)] !! (10 ^ 10))
and for that evaluation order isn't even visible at the denotational level.> Keep in mind that confluence isn't just academic, it's the thing which makes functional programming attractive for parallelism.
Automatic parallelisation of functional languages is academic.
Why guarantee 0 instead of ⊥? I'd say it's due to a general agreement that ⊥ has the lowest desirability: if we have the option of returning ⊥ or something else, we should pick that something else. Non-strict evaluation strategies are the most extreme choice, but most languages consider the desirability of strictness to be stronger than than the undesirability of ⊥.
I don't think exceptions are so simple though. Imagine a situation like this:
getUserById :: [UserDetails] -> UserId -> User
getUserById db id = MkUser id (getName details) (getDOB details)
where details = head (filter ((== id) . getID) db)
On one hand, we may want this to fail fast: if `head` throws an `EmptyList` exception, we want to propagate that to the whole expression. Since getUserById might throw, we can wrap it in exception handlers and deal with missing users appropriately.On the other hand, we may want to ignore exceptions in sub-expressions that we don't care about, e.g. having `fst (0, Exception)` reduce to 0. This seems trickier for `getUserById`, since we might do a bunch of processing which only needs the ID, and end up triggering the `EmptyList` exception far away, deep in the heart of a pure-looking function. I can think of three solutions to this:
- Wrap exception handlers around the subsequent steps. This smells funny, since those steps might be completely pure.
- Jump back to the original exception handler. Such non-local jumps may be very hard to understand, plus the handler would need to work in arbitrary contexts; all it can really do is return a different value of the same type (e.g. some predetermined default), or throw a different exception (which just defers the problem) or produce ⊥.
- Mark potentially-exceptional values somehow, so we can track their propagation through the program, and handle them if needed. That doesn't seem any different than `Maybe` or `Either`, perhaps modulo some lifting.
Of course, the situation becomes even more complicated if an expression contains many different exceptions!
> Automatic parallelisation of functional languages is academic.
Note I said "attractive", not "automatically solves all problems" ;) Even with "manual" parallelism, like `par`, map/reduce, etc. it's nice that these don't alter the semantics.
It also simplifies compiler optimisations, and helps programmers reason about when they will/won't fire.
Then `⊥ || True` would return True, but it doesn't. The reason Haskell specifies how it evaluates values is simply because any language which doesn't is broken. A language with no evaluation order is strictly less usable than one with (any) specified order of evaluation.
> Imagine a situation like this
I am really confused about what you're confused about. If head throws an exception E, and assuming getName and getDOB are nontrivial, getUserById reduces to `MkUser id E E` (this is not a language level statement). There is no jumping or impurity or any such thing happening.
> Note I said "attractive"
To academia, yes.
This claim is a little strong out of context. If you're just talking about Haskell, with the Prelude definition of `||` and no rewriting shenanigans, then you're right. That doesn't mean ⊥ is desirable though; it's just unavoidable in this case, due to constraints imposed by other desirable properties of the language.
Haskell's designers found the semantics of lambda calculus desirable enough to use as a base for Haskell, even though it removes their ability to define such a "parallel or" function.
This is similar to the desirability of strictness: most languages find it compelling, even though it removes the ability to avoid some ⊥ results like in `fst (0, ⊥)`.
> The reason Haskell specifies how it evaluates values is simply because any language which doesn't is broken.
Haskell only constrains evaluation order to be "non-strict". Implementations are free to use any non-strict evaluation order they like, although I agree that any "serious" language implementation should document what users should expect to happen. Note they should also specify what not to expect, e.g. it might say that the evaluation order of arguments is undefined, even if it just-so-happens to work in some predictable way at the moment!
In any case, in your `⊥ || True` example it's not the evaluation order that triggers the ⊥, but the data dependency in the definition of `||`:
x || y = if x
then True
else y
If the language semantics allows something like Conal Elliot's `unamb` operator ( http://conal.net/blog/posts/functional-concurrency-with-unam... ) we could define `||` in a non-strict way but, as I said, Haskell's designers preferred to pick lambda calculus semantics over parallel-or semantics.> If head throws an exception E, and assuming getName and getDOB are nontrivial, getUserById reduces to `MkUser id E E` (this is not a language level statement).
That's the first reduction step. The question is whether or not we should reduce it any further, to get `E`. Strict languages would do this, non-strict ones wouldn't.
If we do perform this (strict) reduction, we'd trigger some ⊥ and exception results unneccessarily, e.g. `getId (MkUser id E E)` would give `E` rather than `id` (and likewise for ⊥ instead of E).
If when we don't do this strict reduction that things get tricky, since we'll end up passing potentially-exceptional values around our program. This is just like Haskell passing around potentially-⊥ values.
The tricky part is handling these exceptions. If we define a handler at the point they're triggered, we'll have to put handlers all over our pure functions. For example the following is a pure function, but if we call it with the `MkUser id E E` value we got from `getUserById` we end up needing a handler for the EmptyList exception:
isMillenial user = dob > 1989-12-31 && dob < 2010-01-01
dob = try (getDOB user) (handle-exception-here)
Alternatively, we could define a handler at the point they're thrown, e.g. safeGetUserById db id default = try (getUserById db id) (exception-handler-goes-here)
Yet `getUserById` doesn't throw an exception (in our non-strict setting), so this handler won't be invoked; we'll just have `MkUser id E E` like before, with the exceptions potentially neing triggered elsewhere.Alternatively, we could "attach" the handler to the result, so if the exceptions get triggered that handler will be invoked. That's the "jumping" I was talking about.
The other difficulty is where do we return to after handling an exception? If our handler's triggered during `isMillenial`, then it had better return a DOB; if it's triggered during `greetUser` then it had better return a UserName, etc.
We then have to consider what to do if a value contains multiple exceptional values, all with different handlers...
> > Note I said "attractive"
> To academia, yes.
Not sure what you're getting at here? "Attractive" doesn't mean "a solved problem which everyone would be mad to avoid", just a nice source of inspiration. Heck, Google's MapReduce is clearly inspired by functional programming ideas like confluence, and that's been out of academia for so long that it's become deprecated!