Coroutines

OmniMark can execute multiple computations concurrently by interleaving the execution of their actions. The interleaved computations are called coroutines.

Coroutines are an important ingredient of the streaming programming paradigm. Every coroutine in OmniMark either produces a stream or consumes it. There are four types of coroutines:

  • string source coroutines that produce a stream of textual data,
  • string sink coroutines which consume a stream of textual data,
  • markup source coroutines, producing a stream of parsed markup, and
  • markup sink coroutines that consume a stream of parsed markup.

Another feature of coroutines in OmniMark is that they always exist in pairs. A string source producer coroutine is always paired with a string sink consumer coroutine, and vice versa. The same relationship exists between the coroutines of markup source and markup sink type; they are always paired together as well. The data stream produced by one coroutine, which it outputs into its #current-output, is fed to and consumed by the other coroutine through its #current-input.

To define a coroutine, use define function syntax with one of the four types listed above: for example, define string sink function. OmniMark will create a new coroutine pair whenever the function is called. The following example will serve to demonstrate some of the concepts behind coroutines in OmniMark:

  define string source function
     producer ()
  as
     using output as #main-output & #current-output
     repeat for integer count
        output "4fkd" % count || "%n"
     again
  
  
  process
     local integer total
  
     output "The numbers that add up to more than 100 are:%n"
     repeat scan producer ()
     match white-space* digit+ => n
        set total to total + n
        exit
           when total > 100
     again
     output "----%n"
     output "4fkd" % total || "%n"
        

In this example, the function producer defines a coroutine producing a string source. When the function is called, OmniMark creates a coroutine pair out of producer and the repeat scan loop that acts as the consumer. After that, the following things happen in order:

  1. Both coroutines are initialized for execution. If producer had any arguments, for example, they would be evaluated and passed to it, and any optional initializers would run as well.
  2. The consumer coroutine always executes first. In our example, that means that repeat scan begins to run.
  3. The match clause attempts to match a pattern against #current-input. Since there is nothing there yet, the consumer suspends and hands control over to producer. If the consumer terminated without trying to consume its input, the producer would not have run at all.
  4. The producer now gets to run. It enters the repeat for loop, outputs the first number (both to the consumer and to #main-output where we can see it) and suspends.
  5. The consumer's match clause consumes the first number, adds it to total, and loops.
  6. The two loops, repeat for and repeat scan, alternate in producing and consuming the numbers …
  7. … until the repeat scan loop is exited and the consumer terminates.
  8. At that point, OmniMark forcibly terminates producer: with no consumer to pair up with, its output is not needed any longer. If producer was left to run on its own, it would never terminate.
  9. After both coroutines are finished, the process rule continues to execute and outputs the final total.

The output of the program is:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
  ----
   105

The two coroutines in a pair form a coroutine scope: OmniMark guarantees that they have the same lifetime by synchronizing their initialization and termination.

The use of coroutines has three principal advantages:

  • It eliminates the need to buffer intermediate results before processing them further.
  • It enables live feedback from the later stages of processing to earlier stages while they are still running.
  • It allows related coroutines to share and update their common state during the processing.

For the most part, you do not need to concern yourself with the mechanics of coroutines. OmniMark handles all the details for you. However, there are some important restrictions you need to be aware of:

If you write code that depends on the interaction between two coroutines, you may need to be aware of the rules OmniMark uses when switching from one coroutine to another.

  • In general, OmniMark switches coroutines at its own discretion. You cannot control the switch, but you may be able to write a test in one routine to see what the other routine is up to. For instance, in a string source function that feeds do xml-parse, you can test to determine what the current element is in the parsing routine using the element is test.
  • There is guaranteed to be a context switch from the producer at the end of its every output action, except as follows.
  • If the only output destination is #markup-parser, there will be no context switch for the duration of a repeat over loop or a using block applied to the attributes shelf, the data-attributes shelf, or a list-valued attribute shelf.