Ok, here's our next needed syntactic change, already implemented and being used in my draft of an 0.8.5 release. Fortunately, this one's completely upwards compatible.
In actually writing real live distributed applications, echat & edesk, MarcS found himself repeatedly needing to turn data flow back into control flow, and repeatedly using the standard E idiom for doing so:
(foo <- msg(args)) whenResolved(define observer(result) {
if (E isBroken(result)) {
define problem := E optProblem(result)
... handle the failure case ...
} else {
... handle the success case ...
}
As you can guess from the title of this email, having to say this manually every time was quite a pain. In fact it was so much of a pain that we worry that people will avoid the whole idiom much to their peril, and drop the failure-handling case. So we have new syntactic sugar to propose: the when/latch/catch construct.
But before we even think about explaining the new sugar, in the next email, let's carefully step through the above idiom and see what's going on.
foo <- msg(args)
This requests foo to "eventually" perform the requested action. This expression immediately returns a promise for a result. foo may be local or remote. Since the remote case is strictly more interesting than the local case, I will assume foo is remote.
By special arrangement, the "whenResolved" message succeeds at requesting notification of resolution in all the above circumstances.
First, "whenResolved" is among the Miranda methods that all objects respond to. Though coded in Java, it's equivalent to:
to whenResolved(observer) {
observer <- run(self)
}
Where by "self", I simply mean the receiving object. In this, whenResolved is identical to Joule's "respond", which is where the idea came from. The cool thing about testing for resolution by sending a tracer message on the reference is that the message snakes its way through intermediate levels of forwarding, such as case #c above.
The only cases left to handle are broken promises and network partitions.
If a whenResolved is sent to a broken promise, the broken promise, by special dispensation, sends itself to the observer argument as in the above Miranda method. If an unforwarded promise becomes broken, it does likewise for any whenResolved messages it had queued up for delivery.
If a whenResolved is sent to a local forwarded promise, or if a local promise with a queued up whenResolved gets forwarded, then the whenResolved just gets sent to its new destination with no special case, just like any other message. This is because local promises are reliable. Once forwarded, they are identical to the thing they are forwarding to.
Not so with remote references, both proxies and futures. A network partition can break the proxy c1 for Carol in VatA, even though Carol in VatC remains perfectly healthy. Whereas a local reference is reliable, a remote reference is fail-stop. Until it fails, it will deliver messages reliably in order. Once it fails, it will deliver no more messages. Of course, the constraints of distributed systems prevent us from knowing precisely when it failed if it fails because of partition. We know the failure happened no earlier than the last message we know successfully got delivered (by receiving a reply), and no later than when we receive notification of partition.
In any case, since remote references are fail-stop rather than reliable, they also treat whenResolved messages specially. They remember the original whenResolved message that came in, and instead transmit one just like it, but with an observer that wraps the original observer. If the network partitions, these remote references become broken, and they send themselves to the original observers, just as with broken promises. If the wrapped observer gets invoked, they forward the invocation to the original observer, and drop the memory of this whenResolved, as the obligation has now been discharged.
Whew! That's a lot of detailed hair, but the resulting semantics are simple. If you send whenResolved to a reference, should the reference become resolved, it will send the resolution (presumably identical to itself) to the observer argument.
Since, in the distributed case, partition may always happen, one should always check this resolution to see if it's broken. "E isBroken(result)" does this. Optionally, if it is broken, "E optProblem(result)" will yield the exception object that explains the problem. If it's not broken, it must represent a successful resolution. I believe that explains the whole idiom. I'll propose our sugar in a later message.
Cheers,
--MarkM