[e-lang] Shortcuts for asynchronous control flow (long) [corrected]

David Hopwood david.nospam.hopwood at blueyonder.co.uk
Wed Jul 5 12:21:49 EDT 2006


Constantine Plotnikov wrote:
> This version is corrected version of the previous message. 
> The following bugs are fixed:
> - eventual calls now use "<-" instead of "."
> - "par" example is added to "seq" example (read and write operations are done in parallel)
> - The "out.write()" is now "out<-write(data)"
> 
> ----------
> I'm still working on Sebyla and there are ideas on asynchronous control 
> flow that might be interesting to E. I plan to introduce OCAM-like "seq" 
> and "par" expressions.

I'm not sure that naming these constructs after the Occam primitives is
a good idea, since they have somewhat different semantics.

"par" in Occam introduces true concurrency; the branches of the par can
be executed in parallel. In an E-like event-loop model this could only
be simulated accurately by creating parallel vats (which is probably not
what you want, since it would impose restrictions on the use within the
"par" construct of refs local to the vat running the surrounding code).

The "pseudo-concurrency" (I think that is what the MarkM calls it) of
objects within a vat is sufficiently different to Occam's "par", that a
different name should be used for a construct that provides it.

> I also plan new expressions named "serialized" 
> and "using" (like one in C#).

> These expressions have obvious expansion to "when" expression. However 
> if there are problems with expansion, I might write about expansion in 
> follow up messages.
> 
> *The "par" expression*
> 
> It is a quick way to organize parallel operations. Basic syntax is the 
> following:
> 
> Par ::= "par" "{"
>   <expression statement> *
> "}"
> 
> 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.

This way of reporting failure may be too elaborate for code using this
construct to handle correctly. That is, properly handling it may be more
involved than manually expanding the "par" construct, and specializing
the expansion and error handling for a particular use.

> The expressions are evaluated independently and each expression is 
> implicitly surrounded with {}. And def construct in one statement has no 
> effect in others.

I'm not sure this fits E's syntax well. Occam interprets statement
boundaries depending on the surrounding construct (alt, par or seq),
whereas E always treats statement boundaries as sequential composition.

> The expression might one of the following:
> 1.  Scalar expression (int, string, etc). In case of this expression the 
> par does not waits for anything.
> 2.  Promise. In that case par waits until promise is resolved.
> 3.  Eventual reference. In that case it waits until reference is resolved.
> 4.  Block. This blocks evaluates just like normal block expression in e. 
> No special rules. It allows to introduce some local scope like {def a = 
> (x+2*pi); a*a;}
> 
> Sample is given in the section of about "seq".
> 
> *The "seq" expression*
> 
> The expression allows serialize actions where one action should be 
> executed only after other finishes. The syntax is the following
> 
> Seq ::= "seq" "{"
>   <expression statement> | <wait statement>*
> "}"
> 
> <wait statement> ::= "wait" <varname> ":=" <expression> ";"
> 
> The expression evaluates each expression statement sequentially. And 
> waits for termination using rules similar to ones from the "par" 
> expression. The expression evaluates to promise for the value of the 
> last expression. If one of statements fails, the expression is smashed 
> with that fault.
>
> Like in "par", defs in expressions could not be seen in other 
> statements. However there is a "wait" statement that allows to introduce 
> value visible in other statements.
> 
> This construct could be useful to create IO loops. For example 
> read/write loop could be written as the following:
> # the function below forwards data and returns total length when 
> finishes writing.
> def forward(in, out, maxReadDataSize): vow[int] {
>   def forward(data, in, out, sum) : any {
>     return seq {
>       wait [_,newData] := par {
>         if(data != null) {
>           out<-write(data)
>         }
>         in<-read(maxDataSize)
>       }
>       if(newData == EOF) {
>         sum
>       } else {
>         forward(newData, in, out, sum + data.length())
>       }
>     }
>   }
>   return forward(null, in, out, 0)
> }

I'm not sure what benefit this example gains over when-catch. I have
also considered a similar construct, but I thought of the main advantage
as being to avoid nested when-catch clauses, resulting in code that
looks more similar to sequential code. This example would have no nested
when-catch.

> Note that this loop creates a lot of promises for returning result, but 
> this could be reduced using compiler (see 
> http://www.eros-os.org/pipermail/e-lang/2004-February/009529.html).
> 
> Another note is that both par and seq could be completely executed 
> in the turn where they are created if all statements return a near 
> result.
> 
> 
> *The "using" expression*
> 
> This expression is inspired by C# "using" statement. The syntax is the 
> following:
> 
> Using :: = "using" "("  ("def" <name> ":=")? <expression> ")" "{"
>   <statement>*
> "}"

Minor nit: a "def" construct in E is already an expression, but it returns
a resolver, not the assigned value (see "def x doesn't return x" in
<http://homepage.mac.com/kpreid/elang/surprise.html>).

This makes the interpretation of this construct ambiguous: it might be
"using" the resolver. Perhaps the best fix is to change "def" (I also find
its current semantics surprising).

Also, the convention is that eventually-executed blocks are written with
"->" before the "{".

> The expression assumes existence of interface similar to the following:
> 
> interface Closable {
>   to close() : any
> }

The spelling is "closeable".

> The expression evaluates to promise for result of the body block. The 
> [using] expression waits until result of <expression> is resolved than it
> checks if the result expression supports closable interface. If it does not 
> supports, the expression fails. If it supports, the value is bound to 
> name, and code block is executed. After value of the block is resolved, 
> the using construct waits until promise from close operation is 
> resolved, and than resolves result promise with value returned from body 
> block.
>
> 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?

> If body is successful but close fails, 
> expression fails with exception from close.
>
> Note that closable might be near object or eventual reference. In 
> case of immediate object, close operation can return null meaning that 
> close operation has finished in this synchronous call. For example, 
> InputStream wrapper could close input stream synchronously using Java 
> close call.

I like this construct, and I think it should be added to the E library
(using lambda-args or whatever).

> For example copy file could be written like the following:
> 
> def copyFile(fileFactory, nameIn, nameOut) : vow[int] {
>   return using(def in := fileFactory<-open(nameIn, "r")) {
>     using(def out := fileFactory<-open(nameOut, "w")) {
     ^ return
>       # function from the "seq" example

Nitpick: that function takes (in, out, maxReadDataSize).

>       forward(in,out)
       ^ return
>     }
>   }
> }

Do we also want to allow

    ... using (def in  := fileFactory <- open(nameIn, "r"),
                   out := fileFactory <- open(nameOut, "w")) -> { ... }

as equivalent to

    ... using (def in := fileFactory <- open(nameIn, "r")) {
      return using (def out, fileFactory <- open(nameOut, "w")) -> { ... }}

?

(Note that "using (def [in, out] := [..., ...])" would not work since
lists are not Closeable.)

BTW, this isn't a good example from a capability security point of view,
since objects are preferred over string names where possible. I know that
wasn't the point of the example, but it would be better written as:

  def copyFile(fileIn :File, fileOut :File) :vow[int] {
    return using (def in  := new FileInputStream(fileIn),
                      out := new FileOutputStream(fileOut)) -> {
        return forward(in, out)
      }
    }
  }

> *The "serialized" expression*
> 
> This expression is most tricky one. It allows to serialize requests to 
> objects that needs it. The problem with asynchronous servers that 
> request might come before previous is finished to serve.
> 
> To solve this problem the following utility object is introduced to 
> standard library.
> 
> def makeRequestQueue() {
>   /** this returns Closable that closes current operation and start next 
>    * operation. The close operation returns nil */
>   def makeClosable() :Closable {
>     ...
>   }
>   return object {
>     /** this method tries to start operation immediately, if there is
>      * a pending request it returns null, which means that operation
>      * cannot be started. */
>     to tryStartRequest() :Closable {
>        ...
>     }
>     /** start operation when possible. */
>     to startRequest() :vow[Closable] {
>        ...
>     }
>   }
> }
> 
> For implementation of this class see the following Java source that I 
> have created for asyncobjects framework.
> 
> http://svn.sourceforge.net/viewcvs.cgi/asyncobjects/asyncobjects/trunk/src/net.sf.asyncobjects/src/net/sf/asyncobjects/util/RequestQueue.java?view=markup&rev=10

> The syntax of expression is the following:
> 
> serialized(<expression>) {
>   <body>
> }
> 
> The expression is expanded like the following:
> 
> {
>   def rq := <expression>
>   def cl := rq.tryStartRequest()
>   if(cl != null) {
>     using(cl) {
>       <body>
>     }
>   } else {
>     using(rq.startRequest()) {
>       <body>
>     }
>   }
> }

I'm not sure what this is doing differently than just

  using (<expression>.startRequest()) -> {
    <body>
  }

which would not need syntactic sugar (especially if "startRequest" is
renamed to just "start" or "request".)

> The construct can be used like the following (the code also demonstrates 
> usage of some other expressions):

I will have to look at this code more closely.

-- 
David Hopwood <david.nospam.hopwood at blueyonder.co.uk>




More information about the e-lang mailing list