[e-lang] Shortcuts for asynchronous control flow
David Hopwood
david.nospam.hopwood at blueyonder.co.uk
Thu Jul 6 13:53:41 EDT 2006
Constantine Plotnikov wrote:
> Below are some comments to issues posted by you. Grammar/spelling/bugs
> comments are mostly accepted.
>
> *Issues with par/seq*
>
> 1. The "par" and "seq" are good names. And there is no reason to drop
> good names because someone else used them differently. As for
> differences, I guess it would be a bad idea to drop "def" because python
> uses it differently ;).
The difference is that the names "par" and "seq" look, to anyone who knows
Occam, as if they have specifically been taken from that language. So I think
that the semantics should be close to that of the Occam constructs, if these
names are to be used.
> The "seq" has mostly the same semantics. The "par" statement in Occam
> has different semantics but sufficiently close one. Note that while
> parallel activities are initiated in the same vat. They might happen in
> different vats. I expect that code in par/seq will mostly waits while
> someone else will do real job (os, hardware, other parties). If real job
> is done in par's branches, the par would be a special case of seq.
>
> However other name could be chosen instead of "par". For example, "all"
> looks like a good alternative.
Yes, using "all" (or infix "|||"; see below) would address this point.
> It still short and possibly captures the
> idea even better. I will think about it.
>
> 2. The sample is a bit more tricky than it looks. It has the following
> properties that are should be kept if you want to re-express it with
> when/catch.
> - Read and write operations are done in parallel.
> - While write is active no other write is performed.
> I do not see how you would write it without multiple when/catch. My
> variant is:
>
> def forward(in, out, maxReadDataSize): vow[int] {
> def forward(data, in, out, sum) : any {
> def readPromise := in<-read(maxReadDataSize)
> # this part is a bit of cheating because we do not need result of write
> # beyond that it has not failed otherwise code will be more complex
> def readAndWritePromise := if(data != null) {
> when(out<-write())->done(_) {
^^^^^^^ write(data)
> return readPromise
> } catch(e) {
> throw e
> }
> } else {
> readPromise
> }
> return when(readAndWritePromise) ->done(newData) {
> return if(newData == EOF) {
> sum
> } else {
> forward(newData, in, out, sum + data.length())
> }
> } catch(e) {
> throw e
> }
> }
> return forward(null, in, out, 0)
> }
This can be simplified using "easy-when", together with the sugar
for waiting on more than one promise (chapter 18 of
<http://www.erights.org/talks/thesis/>):
pragma.disable("explicit-result-guard")
pragma.enable("easy-when")
def forward(in, out, maxReadDataSize) :vow[int] {
def forward(data, in, out, sum) :any {
return when (
if (data != null) { out <- write(data) },
def newData := in <- read(maxReadDataSize),
) -> {
return if (newData == EOF) {
sum
} else {
forward(newData, in, out, sum + data.length())
}
}
}
return forward(null, in, out, 0)
}
I don't think this is any less readable than the seq/par version.
> We could also consider other example that compares streams by byte. It
> assumed that readByte returns either byte or EOF and EOF is different
> from any byte value. It is also assumed that EOF == EOF.
>
> # seq/par version
> def compareStreamsByByte(in1, in2) : vow[boolean] {
> return seq {
> wait [b1, b2] := par {
> in1<-readByte()
> in2<-readByte()
> }
> if(b1 != b2) {
> false
> } else {
> compareStreamsByByte(in1, in2)
> }
> }
> }
>
> # when/catch version
> def compareStreamsByByte(in1, in2) : vow[boolean] {
> def b1Vow := in1<-readByte()
> def b2Vow := in2<-readByte()
> def pair := when(b1Vow) ->done(b1) {
> return when(b2Vow)->done(b2) {
> if(b1 != b2) {
> false
> } else {
> compareStreamsByByte(in1, in2)
> }
> } catch(e) {
> throw e
> }
> } catch(e) {
> throw e
> }
> }
>
> To me seq/par is more readable. But I guess the results could differ.
This also benefits from "easy-when" and waiting on more than one promise:
pragma.disable("explicit-result-guard")
pragma.enable("easy-when")
def compareStreamsByByte(in1, in2) :vow[boolean] {
def b1 := in1 <- readByte()
def b2 := in2 <- readByte()
return when (b1, b2) -> {
if (b1 != b2) {
return false
} else {
return compareStreamsByByte(in1, in2)
}
}
}
Again, this is no less readable than the seq/par version.
> 3. Problem with statement boundaries could be solved by some prefix to
> expression in order to reduce expectation (for example "<-"). Like in
> sample below:
>
> def compareStreamsByByte(in1, in2) : vow[boolean] {
> return seq {
> wait [b1, b2] := par {
> <- in1<-readByte()
> <- in2<-readByte()
> }
> <- if(b1 != b2) {
> false
> } else {
> compareStreamsByByte(in1, in2)
> }
> }
> }
> Other alternative would be to explicitly surround expressions with {}.
>
> def compareStreamsByByte(in1, in2) : vow[boolean] {
> return seq {
> wait [b1, b2] := par { {in1<-readByte()} ; {in2<-readByte()} }
> {
> if(b1 != b2) {
> false
> } else {
> compareStreamsByByte(in1, in2)
> }
> }
> }
> }
{...} is currently a "hide" expression.
> However I would like it to stay the way it was originally proposed.
> Blocks and prefixes will introduce visual clutter and will reduce
> usefulness of expressions.
Something like
all {
...
} and {
...
} and {
...
}
would be better IMHO (similarly, I would prefer "seq {...} then {...}".)
Alternatively, the syntax could be inspired by the CSP infix interleaving
operator, "|||":
{...}
||| {...}
||| {...}
e.g.
wait [b1, b2] := {in1 <- readByte()} ||| {in2 <- readByte()}
(See section 3.6, page 99 of <http://www.usingcsp.com/cspbook.pdf> for a
description of interleaving in CSP. Your "par" is closer to interleaving
than it is to Occam's "par" or CSP's "||", I think.)
> 4. This semantics...
# The par construct wait until all promises are resolved and return a
# promise for tuple that contains result of all expressions inside. If
# some of calls fails the result promise is smashed with a fault that
# contains a tuple with partial result (null for failed positions) and a
# tuple with faults.
> allows to report outcome of "par" completely in case of
> failure. I do not see other option to report it completely. Only few
> needs that complex failure semantics. Most will likely need to just
> rethrow exception or to log it. For this purpose, complex structure of
> the exception is no problem, particularly if logging layer will be aware
> of this structure.
OK, you have a point here.
> *Using issues*
>
> 1.
>
>
>>>If body block fails, close operation is executed anyway and expression
>>>fails with exception from body.
>>
>>It fails with the body exception even if the close also fails, I assume?
>
> In Java/C# it fails with exception from close in this case.
I think that would be a design error. The body exception is the first failure,
and therefore more likely to carry useful information about the cause of the
problem. Note that the close could fail *because* the body failed, leaving the
stream in an inconsistent state (this shouldn't happen, but might if the stream
implementation is not defensively consistent).
Another possibility is to throw a wrapper exception that encapsulates both
the body exception and the close exception. This at least does not lose
information.
> In Sebyla, I plan to reproduce this behavior. The semantics in E must match
> when/finally semantics in E and I do not remember it now.
>
> 2. My complaint about "def" is even bigger. I do not like that "def" is
> an expression. I think that the name declarations should be statements
> rather than parts of expressions. Using it in expression makes it
> difficult to understand true scope of the definition.
What I said about "def" in my previous post was wrong ("def x := y" returns y,
not the resolver for x), so this issue probably isn't relevant.
> 3. "using" with multiple resource is a good idea.
>
> 4. I suggest other version of sample:
>
> def copyFile(fileIn :File, fileOut :File) :vow[int] {
> return using (def in := fileIn<-openInputStream(),
> out := fileOut<-openOutputStream()) -> {
> return forward(in, out)
> }
> }
> }
Yes, that's better. (I specifically looked whether there were already
methods to open I/O streams on a File -- there aren't, but there probably
should be in the E-sugared version of File.)
> Note that it does not dictate stream implementation. And stream can be
> created in other vats.
I think the stream should be created in the same vat, but that's not really
important.
> *Serialized issues*
>
> Note that main point of the construct is to allow optimization of
> execution it in the same turn when there are no pending requests in
> progress. If this feature is not used, then there is no point in it,
> except of better displaying intention in the source.
OK, but I'm skeptical that this optimization justifies a new syntactic
abstraction.
--
David Hopwood <david.nospam.hopwood at blueyonder.co.uk>
More information about the e-lang
mailing list