[E-Lang] pending revision of E in a Walnut
Mark S. Miller
markm@caplet.com
Mon, 27 Aug 2001 12:48:33 -0700
At 03:57 PM Sunday 8/26/01, Mark Seaborn wrote:
>A problem I have with exception-handling constructs is that they tend
>to be too liberal in where they catch exceptions from.
Curiously, your other proposal in this same email admits two expansions. One
consistent with the new "too liberal" mixed control-flow dataflow nature of
when-catch, and the one you present, consistent with the earlier pure
dataflow form. In principle, both allow problems in the when body to be
caught and reported, which was the requirement motivating the new too
liberal form. Unfortunately, there's still a tradeoff, as shown below.
>On a related topic, a common idiom in E seems to be to create
>(promise, resolver) pairs in order to return the promise immediately
>and resolve it in the `done' or `catch' clause of a `when' expression.
>But since `when' currently doesn't return any interesting value
>(correct me if I'm wrong on this),
You are correct. A when-catch expression currently always evaluates to 'null'.
>why not make `when' return a
>promise for the value that is returned by the `done' or `catch' clause
>(with the promise becoming broken if the clause executed throws an
>exception)?
I like this suggestion a lot. Very clever. Accepted in principle. On to
the details.
>In other words, make
>
> when(X) -> done(x) { Y }
> catch e { Z }
Btw, that needs to be written
when(X) -> done(x) { Y
} catch e { Z }
or, more conventionally
when(X) -> done(x) {
Y
} catch e {
Z
}
such is the price of the newline-significance that enables E to also be a
shell language.
Since the "done(x) {" is supposed to look like a function header, and since
the expansion does in fact define a function of one argument, I propose
that, to get the effect you seek, you would need to write
"done (x) :resultGuard {", which I will assume below.
>give the behaviour currently given by
>
> def [promise, resolver] := PromiseMaker()
> when(X) -> done(x) {
> try { resolver resolve(Y)
> } catch e { resolver smash(e) }
> } catch e {
> try { resolver resolve(Z)
> } catch e2 { resolver smash(e2) }
> }
> promise
[Curlies above corrected according to previous explanation. ":resultGuard"
ignored for now.]
This would be upwards compatible from the old pure dataflow-oriented
when-catch, while in principle fixing its fatal flaw: a problem thrown by Y
would still, in principle, be reported to someone -- it would break the
promise that the when-catch evaluated to. Contrast with the following
expansion, which would be upwards compatible from the new "too liberal"
mixed control-flow/dataflow oriented when-catch:
def [promise, resolver] := PromiseMaker()
Ref whenResolved(X, def done(temp) {
try {
def result :resultGuard := try {
if (Ref isBroken(temp)) {
throw(Ref optProblem(temp))
}
def x := temp
Y
} catch e {
Z
}
resolver resolve(result)
} catch e2 {
resolver smash(e2)
}
})
promise
Note that the inner try-catch corresponds to the current expansion.
In both expansions, if Y or Z is evaluated and it successfully evaluates to
a value (that's successfully coerced by :resultGuard), then the promise will
resolve to that value. Also, in both, if Z is evaluated and it throws a
problem, the promise will be smashed with that problem.
The difference between the two is in what happens if Y is evaluated and
throws an exception. In the first expansion, the problem is reported only
by smashing the promise. In the second, the problem is reported only to the
catch of the when-catch. While I see that the first is purer, an E
programmer would typically only receive such an error report if they did
another when-catch on the promise for the result of the first when-catch.
An interesting tradeoff. My current inclination is to go with the semantics
shown by the second expansion, but do it by modifying the
"Ref whenResolved(..)" method to create, return, and resolve the promise,
rather than making the expansion do this. The expansion would be modified
only to allow a ":resultGuard", which would become the guard on the "done"
function, just as it looks like it is.
when(X) -> done(x) :R {
Y
} catch e {
Z
}
would expand to:
Ref whenResolved(X, def done(temp) :R {
try {
if (Ref isBroken(temp)) {
throw(Ref optProblem(temp))
}
def x := temp
Y
} catch e {
Z
}
})
>[...] Also the expansion of `when' could probably remove
>the use of `try' by using eventual sends for turning control-flow
>exceptions into data-flow exceptions.)
Quite correct. The logic for invoking the done function and resolving the
promise, now moved into the internals of "Ref whenResolved(..)", could
either be of the form
try {
resolver resolve(doneFunc(x))
} catch e2 {
resolver smash(e2)
}
or
resolver resolve(doneFunc <- (x))
The latter would postpone the doneFunc invocation for another turn, but this
is allowed by the semantics.
I'm inclined to make the same change to "Ref whenBroken(..)".
>Using PromiseMaker explicitly creates the risk that a resolver will
>never get called, which could cause datalock. Extending the behaviour
>of `when' gives a way of eliminating the use of PromiseMaker, reducing
>that risk and making code shorter and clearer.
This add another case -- Lost Signal -- to our list of lost liveness bugs.
See the new updated
http://www.erights.org/elib/concurrency/event-loop.html#deadness .
Thanks!
>This extension provides a path for exceptions from the `done' and
>`catch' clauses to flow along. If no result flows out from the `when'
>expression, odds are that those exceptions don't make any difference
>and don't need to be reported.
My concern is losing the problem report when a when-catch is used without
using the promise it evaluates to, which I believe will still be typical use.
Cheers,
--MarkM