[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