Coroutines normally communicate through string source
and string sink
values. Sometimes, however,
two coroutines need to communicate some out-of-band information that cannot be encoded into this stream. Examples
of this kind of out-of-band information are meta-data and processing exceptions.
One way of communicating such information is through shared or global
variables. The problem with this
technique is that it can be difficult to synchronize the modification of a shared variable by one coroutine and
its reading by another coroutine. In order to make a variable modification immediately visible, the coroutine that
modifies the variable must immediately after switch to the variable-reading coroutine, and the latter must
immediately observe the variable change.
The signal
action solves the synchronization problem by immediately raising a throw in the target
coroutine, and then resuming its execution. After the target coroutine catches the throw, it can handle the
communicated data in a catch
clause.
Signals can also be used for two-way communication. Since the target of the signal
action can be either a
string sink
or a string source
, the catching coroutine can signal
back to the other
coroutine.
The example string sink function
multiple-file-writer defined below writes a long stream of
data into multiple files. In this case, file names represent out-of-band information. The first version
communicates the file names through the shared variable file-name. The variable is declared
read-only
in order to pass a reference to the shelf value, not the value itself, to
multiple-file-writer. That way any modification to the original variable will be visible to
multiple-file-writer.
define string sink function multiple-file-writer read-only string file-name as repeat scan #current-input match lookahead any local string current-file-name initial { file-name } using output as file current-file-name repeat scan #current-input match (any (when current-file-name = file-name)) => one output one again again process local string file-name using output as multiple-file-writer file-name do set file-name to "first.txt" output "Contents of the first file.%n" set file-name to "second.txt" output "Contents of the second file.%n" output "Some more contents of the second file.%n" set file-name to "third.txt" ; the third file is empty set file-name to "fourth.txt" output "Contents of the fourth file.%n" done
If we run this program, we'll discover that the empty file "third.txt" has not been created. The reason for this omission is that the multiple-file-writer coroutine does not resume its execution between two modifications of the shared variable file-name. An OmniMark coroutine resumes execution only when it is given or asked for more data, not every time global state should change.
Another problem with multiple-file-writer as written is that it has to manually check if the file-name has changed after every single byte of the input. If it tried to consume and write multiple bytes at once, it could miss a change of the current file name. This is clearly inefficient.
Both of the above problems can be solved if we use signal
instead of a shared variable. To that end, we
declare a catch file-change
that carries information about the file name, and instead of modifying the
shared variable we signal throw file-change
before we supply the contents of the next file.
declare catch file-change value string file-name define string sink function multiple-file-writer as local string current-file-name do assert !(#current-input matches any) message "No file name has been given to write data into." catch file-change file-name set current-file-name to file-name repeat using output as file current-file-name do scan #current-input match any* => contents output contents done exit catch file-change file-name set current-file-name to file-name again done process using output as multiple-file-writer do signal throw file-change "first.txt" output "Contents of the first file.%n" signal throw file-change "second.txt" output "Contents of the second file.%n" output "Some more contents of the second file.%n" signal throw file-change "third.txt" ; the third file is empty signal throw file-change "fourth.txt" output "Contents of the fourth file.%n" done
Note that multiple-file-writer contains no manual check for a file name change after every charater.
It simply consumes its entire normal input at the line match any* => contents
. This pattern will be
terminated when it reaches either a signal
or value-end
in the input. In the former case, the
signal will be propagated into a throw in the local scope as soon as pattern-matching scope do scan
is
finished, and then it will be caught and handled by the catch
clause.
There is one more performance problem remaining in the code. Although the line match any* => contents
consumes the input very quickly, all this input is buffered in memory until the next signal or end of input. The
entire contents then gets written into the file by the following line. We can eliminate the unnecessary
buffering by replacing the pattern-matching scope
do scan #current-input match any* => contents output contents done
by the following line:
output #current-input take any*