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

Constantine Plotnikov cap at isg.axmor.com
Wed Jul 5 15:39:28 EDT 2006


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 "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 othe name could be chosen instead of "par". For example, "all" 
looks like a good alternative. 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(_) {
        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)
}


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.

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)
      }
    }
  }
}

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.

4. This semantics allows to report outcome of "par" completly in case of 
failure. I do not see other option to report it completly. 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.

*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. 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 compaint about "def" is even bigger. I do not like that "def" is 
an expression. I think that the name declaraions shold be statements 
rather than parts of expressions. Using it in expression makes it 
difficult to understand true scope of the definition.

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)
      }
    }
  }

Note that it does not dictate stream implementation. And stream can be 
created in other vats.

*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.

Constantine


More information about the e-lang mailing list