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:
repeat scan
begins
to run.
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.
repeat for
loop, outputs the first number (both to
the consumer and to #main-output
where we can see it) and suspends.
match
clause consumes the first number, adds it to total, and loops.
repeat for
and repeat scan
, alternate in producing and consuming the
numbers …
repeat scan
loop is exited and the consumer terminates.
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:
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:
save
and save-clear
operations are specific to the coroutine in which they
occur, if performed on a global
declared with the domain-bound
qualifier.
using nested-referents
can only be used in one coroutine at a time.
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.
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.
output
action, except as follows.
#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.