Maybe a small example can help to illustrate the idea of using functions which can yield multiple times (like functions returning a Publisher
or a generator or coroutine) from imperative synchronous activities.
Lets assume we have this function which returns count
stock quotes for a ticker symbol:
func stockPrice(symbol: String, count: Int? = nil) -> AnyPublisher<Float, UnknownSymbol>
BTW. an alternative signature of that function might look something like this:
func stockPrice(symbol: String, count: Int? = nil) async(price: Float) throws
Now, this function is used in an activity which should print out the stock price a given number of times or until a maximum price is reached:
01 act printStockPrice(symbol: String, count: Int, maxPrice: Float) {
02 print("print stock price for: \(symbol)")
03 var price: Float
04 cobegin {
05 receive(price) stockPrice(symbol: symbol, count: count)
06 print("exceeded count")
07 }
08 with {
09 repeat {
10 await true
11 print("quote: \(price)")
12 } while price < maxPrice
13 print("exceeded maxPrice")
14 }
15 print("done")
16 }
First, the activity proceeds like in a normal function with a statement to print the symbol (line 2).
The call to the asynchronous stockPrice
function happens in line 5 by using the receive
keyword which allows to bind a variable to the values generated by stockPrice
.
As the control-flow stays at this statement, handling of a received value has to happen in a concurrent trail:
The block introduced by the cobegin
keyword in line 4 starts a first trail (in which receive
is called) and the with
keyword in line 8 starts a second trail where the printing of the quote values will happen,
The second trail consists of a loop (lines 9-12) which first hits the statement await true
in line 10. This will stop the trail until the whole activity is triggered to react again - which will happen once a new value from the stockPrice
publisher is received and bound to the variable price
in line 5. The print statement in line 11 will then print the value and the loop will either continue or break dependent on the condition (12).
When either trail finishes, the other will be preempted (weakly). When, for example, the condition in line 12 causes the second trail to finish, the first trail will be preempted causing the subscription to the publisher done in the receive statement of line 5 to be canceled.
Synchronous activities thus nicely allow the imperative processing of functional reactive streams.