Advanced Control Flow

Thus far, the only control flow mechanism you've been shown was the label construct for creating labeled scopes. But this one construct was powerful enough to express early returns from functions, early breaks from loops, and also (though we haven't shown it) early jumps to the next loop iteration. Each of the above functionality has traditionally been a separate keyword and language feature in other languages, but they're all expressible with just the label construct in Stanza. It is a powerful and general mechanism.

In actuality, Stanza only has a single control flow mechanism, called targetable coroutines. The label construct is just a common usage pattern for them. In this chapter we'll learn about the other common usage pattern of coroutines: attempts and failures, exception handlers, and generators. At the very end, we'll show you the general coroutine construct, along with some examples demonstrating their use.

First Class Labeled Scopes

Here is a function that finds the smallest power of two that is greater or equal to the given argument, n.

defn min-pow-2 (n:Int) :
   label<Int> return :
      var x = 1
      while true :
         if x >= n : return(x)
         x = x * 2
      fatal("Unreachable")   

Let's try it out.

defn main () :
   defn test (n:Int) :
      println("The minimum power of 2 greater or equal to %_ is %_." %
              [n, min-pow-2(n)])
   test(10)
   test(100)
   test(1000)
   test(10000)
   test(100000)
   test(1000000)

main()

Compiling and running the above prints out

The minimum power of 2 greater or equal to 10 is 16.
The minimum power of 2 greater or equal to 100 is 128.
The minimum power of 2 greater or equal to 1000 is 1024.
The minimum power of 2 greater or equal to 10000 is 16384.
The minimum power of 2 greater or equal to 100000 is 131072.
The minimum power of 2 greater or equal to 1000000 is 1048576.

Pulling Out the Body

That's fairly standard so far. But now let's pull out the body of the while loop into a separate function, called pow-2?. It accepts an argument, x, that is the current number being tested, an argument, n, as the limit we're trying to reach, and also a function called return, which we'll explain later.

defn pow-2? (x:Int, n:Int, return:Int -> Void) -> Int :
   if x >= n : return(x)
   else : x * 2

pow-2? first checks whether x is greater or equal to n, and calls return with x if it is. Otherwise it returns the next value of x to test, which is x * 2.

We now update the min-pow-2 function to call pow-2?.

defn min-pow-2 (n:Int) :
   label<Int> return :
      var x = 1
      while true :
         x = pow-2?(x, n, return)
      fatal("Unreachable")

Compile and run the program to verify that it still works.

What is happening here!? We've somehow passed the exit function out of min-pow-2 and into pow-2?. Then when pow-2? called return, it returned not from pow-2?, but from min-pow-2!.

Let's review the definition of the label construct. Here is its general form.

label<T> exit :
   body

The label construct requires the type it returns, T, the name of the exit function, exit, and the body to execute, bodylabel creates an exit function of type T -> Void with the name exit, and then executes body. If body never calls the exit function then the result of body is returned by label. If body calls the exit function then label immediately returns the argument passed to the exit function. The return type Void for the exit function indicates that it doesn't return to its caller.

There is nothing in the description of label preventing us from passing out the exit function, so the call to return in pow-2? is simply causing the label construct to return x, which is then returned by min-pow-2.

Storing the Exit Function

We can even store the exit function in a variable if we like. Here's a global variable

var RETURN: Int -> Void

into which we will store the exit function. Thus the exit function will no longer be passed in to pow-2? as an argument.

defn pow-2? (x:Int, n:Int) -> Int :
   if x >= n : RETURN(x)
   else : x * 2

It will instead be stored in the global variable before pow-2? is called.

defn min-pow-2 (n:Int) :
   label<Int> return :
      RETURN = return
      var x = 1
      while true :        
         x = pow-2?(x, n)
      fatal("Unreachable")

Compile and verify that the program still works as before.

Here you're starting to see just how flexible the label construct really is. Storing the exit function seems like a strange thing to want to do but keep it in the back of your mind as we talk about the other constructs.

Dynamic Wind

One of the issues that accompanies having a powerful control flow mechanism is that in a sequence of expressions, evaluating the first expression does not guarantee that the last one will be evaluated.

f()
g()
h()

For example, in the above sequence, even after f returns, there is no guarantee that h will be called. If g calls an exit function then h will be skipped entirely.

Let us suppose that we do not want to pass in the limit, n, as an argument to pow-2?. We would like to keep it stored in a global variable called LIMIT and set it to the appropriate value only during the call to pow-2?. At all other times, LIMIT should retain its initial value of 0. Here is an initial attempt.

var LIMIT: Int = 0
var RETURN: Int -> Void

defn pow-2? (x:Int) -> Int :
   if x >= LIMIT : RETURN(x)
   else : x * 2

defn min-pow-2 (n:Int) :
   label<Int> return :
      RETURN = return     
      var x = 1
      while true :
         val old-limit = LIMIT
         LIMIT = n
         x = pow-2?(x)
         LIMIT = old-limit
      fatal("Unreachable")

defn main () :
   defn test (n:Int) :
      println("The minimum power of 2 greater or equal to %_ is %_." %
              [n, min-pow-2(n)])
   test(10)
   test(100)
   test(1000)
   test(10000)
   test(100000)
   test(1000000)

main()
println("After main, LIMIT is %_." % [LIMIT])

Printing and compiling the above prints out

The minimum power of 2 greater or equal to 10 is 16.
The minimum power of 2 greater or equal to 100 is 128.
The minimum power of 2 greater or equal to 1000 is 1024.
The minimum power of 2 greater or equal to 10000 is 16384.
The minimum power of 2 greater or equal to 100000 is 131072.
The minimum power of 2 greater or equal to 1000000 is 1048576.
After main, LIMIT is 1000000.

So min-pow-2 is working correctly, but LIMIT is not being restored back to its original value. What is happening? Well the call to pow-2? in

LIMIT = n
x = pow-2?(x)
LIMIT = old-limit

may call the exit function, return. If that happens, then the label construct immediately returns and the last LIMIT = old-limit expression is never evaluated. 

Stanza provides the special function dynamic-wind to handle these situations. It allows you to surround a body of code between some wind in and wind out code. The wind in code is guaranteed to execute whenever the control flow enters the body, and the wind out code is guaranteed to execute whenever the control flow exits the body. Here is how it's used.

defn min-pow-2 (n:Int) :
   label<Int> return :
      RETURN = return     
      var x = 1
      while true :
         val old-limit = LIMIT
         dynamic-wind(
            fn () :
               LIMIT = n
            fn () :  
               x = pow-2?(x)
            fn (final) :           
               LIMIT = old-limit)
      fatal("Unreachable")

The wind in, body, and wind out code is given to dynamic-wind as three anonymous functions. The final argument for the wind out code is a boolean value that indicates whether it is guaranteed to be the last time the wind out code is called. In this example, final will always be true

Now compiling and running the program again prints out

The minimum power of 2 greater or equal to 10 is 16.
The minimum power of 2 greater or equal to 100 is 128.
The minimum power of 2 greater or equal to 1000 is 1024.
The minimum power of 2 greater or equal to 10000 is 16384.
The minimum power of 2 greater or equal to 100000 is 131072.
The minimum power of 2 greater or equal to 1000000 is 1048576.
After main, LIMIT is 0.

indicating the LIMIT is properly being reset to its original value.

Dynamically Scoped Variables

In the above example, we say that LIMIT is being used as a dynamically scoped variable. This is a common pattern and Stanza provides a syntactic shorthand for our call to dynamic-wind.

Here is the min-pow-2 function written using the let-var shorthand.

defn min-pow-2 (n:Int) :
   label<Int> return :
      RETURN = return     
      var x = 1
      while true :
         let-var LIMIT = n :
            x = pow-2?(x)
      fatal("Unreachable")

The general form of let-var is

let-var x = v :
   body

It temporarily sets the x variable to the value v before executing bodyx is restored to its previous value after body is finished executing.

Attempts and Failures

Attempts and failures are syntactic sugar for another use case of targetable coroutines that operate very similarly to labeled scopes. Here is an example.

defn read-letter (xs:Seq<Char>) -> Char :
   if not letter?(peek(xs)) : fail()
   next(xs)

defn read-digit (xs:Seq<Char>) -> Char :
   if not digit?(peek(xs)) : fail()
   next(xs)

defn read-all (xs:Collection<Char>) :
   val xs-seq = to-seq(xs)
   while not empty?(xs-seq) :
      attempt :
         println("Read letter: %_" % [read-letter(xs-seq)])
      else attempt :
         println("Read digit: %_" % [read-digit(xs-seq)])
      else :
         println("Read something else: %~" % [next(xs-seq)])

read-all("42 is the answer.")         

Compiling the above prints out

Read digit: 4
Read digit: 2
Read something else: ' '
Read letter: i
Read letter: s
Read something else: ' '
Read letter: t
Read letter: h
Read letter: e
Read something else: ' '
Read letter: a
Read letter: n
Read letter: s
Read letter: w
Read letter: e
Read letter: r
Read something else: '.'

The function read-all calls read-letter and read-digit in an attempt block. If the block evaluates without ever calling fail then the result of the block is returned by attempt. If the block calls fail, then attempt immediately returns the result of evaluating the code in the else branch.

Example: S-Expression Parser

Here is an example of using attempt and fail to program a simple s-expression parser. An s-expression, in this case, will be defined as either

  1. a positive integer,
  2. a symbol consisting of letters,
  3. or a list containing more s-expressions.

Overall Structure

Here is the basic structure of the parser.

defn parse-sexp (sexp:String) -> List :
   val chars = to-seq(sexp)

   defn eat-while (pred?: Char -> True|False) -> String :
      ...

   defn eat-whitespace () -> False :
      ... calls eat-while ...
  
   defn parse-symbol () -> Symbol :
      ... calls eat-while ...
        
   defn parse-number () -> Int :
      ... calls eat-while ...

   defn parse-sequence () -> List :
      ... calls eat-whitespace, parse-symbol, parse-number, and parse-list
     
   defn parse-list () -> List :
      ... calls parse-list ...

   parse-sequence()
   ...

parse-sexp is given a string, and returns a list of s-expressions. Upon entering the function, we ask to view the string as a sequence of characters, charseat-while and eat-whitespace are helper functions. parse-symbolparse-numberparse-sequence, and parse-list are mutually recursive functions that parse symbols, numbers, sequences of s-expressions, and lists of s-expressions.

Helper Functions

The eat-while, and eat-whitespace functions are helper functions for reading from charseat-while takes a predicate function, pred?, and eats characters from chars as long as pred? returns true. It returns a string containing the eaten characters. eat-whitespace eats all leading spaces in chars. Here are their definitions.

defn eat-while (pred?: Char -> True|False) -> String :
   string-join(take-while(pred?, chars))

defn eat-whitespace () -> False :
   eat-while({_ == ' '})
   false

Parsing Symbols

The parse-symbol function eats and returns the next symbol from chars. If the next character in chars is not a letter, then parse-symbol fails.

defn parse-symbol () -> Symbol :
   if not letter?(peek(chars)) : fail()
   to-symbol(eat-while(letter?))

Parsing Numbers

The parse-number function eats and returns the next positive integer from chars. If the next character in chars is not a digit, or if the number cannot be represented in 32 bits, then parse-number fails. 

defn parse-number () -> Int :
   if not digit?(peek(chars)) : fail()
   val x = to-int(eat-while(digit?))
   if x is-not Int : fail()
   x as Int

Parsing Sequences

The parse-sequence function reads as many s-expressions as possible by calling parse-symbolparse-number, and parse-list repeatedly.

defn parse-sequence () -> List :
   eat-whitespace()
   if empty?(chars) :
      List()
   else :
      attempt : cons(parse-symbol(), parse-sequence())
      else attempt : cons(parse-number(), parse-sequence())
      else attempt : cons(parse-list(), parse-sequence())
      else : List()

Notice the use of attempt to first try parsing a symbol, and then if that fails to then try parsing a number, followed by trying to parse a list. 

Parsing Lists

The parse-list function eats and returns the next list from chars. A list is simply a sequence of s-expressions surrounded by () characters. If the next character is not an opening parenthesis then parse-list fails.

defn parse-list () -> List :   
   if peek(chars) != '(' : fail()
   next(chars)
   val items = parse-sequence()
   if empty?(chars) : fatal("Unclosed opening parenthesis.")
   else if peek(chars) == ')' : (next(chars), items)
   else : fatal("Expected closing parenthesis but got %~." % [next(chars)])

Driver

Finally, to start off the function, we attempt to read as many s-expressions as possible from chars using parse-sequence

val items = parse-sequence()
if empty?(chars) : items
else : fatal("Unexpected character: %~." % [next(chars)])

Listing

Here is the complete definition of parse-sexp.

defn parse-sexp (sexp:String) :
   val chars = to-seq(sexp)

   defn eat-while (pred?: Char -> True|False) -> String :
      string-join(take-while(pred?, chars))

   defn eat-whitespace () -> False :
      eat-while({_ == ' '})
      false
  
   defn parse-symbol () -> Symbol :
      if not letter?(peek(chars)) : fail()
      to-symbol(eat-while(letter?))
        
   defn parse-number () -> Int :
      if not digit?(peek(chars)) : fail()
      val x = to-int(eat-while(digit?))
      if x is-not Int : fail()
      x as Int

   defn parse-sequence () -> List :
      eat-whitespace()
      if empty?(chars) :
         List()
      else :
         attempt : cons(parse-symbol(), parse-sequence())
         else attempt : cons(parse-number(), parse-sequence())
         else attempt : cons(parse-list(), parse-sequence())
         else : List()
     
   defn parse-list () -> List :  
      if peek(chars) != '(' : fail()
      next(chars)
      val items = parse-sequence()
      if empty?(chars) : fatal("Unclosed opening parenthesis.")
      else if peek(chars) == ')' : (next(chars), items)
      else : fatal("Expected closing parenthesis but got %~." % [next(chars)])

   val items = parse-sequence()
   if empty?(chars) : items
   else : fatal("Unexpected character: %~." % [next(chars)])

Trying it Out

Let's try it out on the following string.

do(println, parse-sexp("This (is) (commonly (called an (S) (Expression)))"))

When compiled and ran it prints out

This
(is)
(commonly (called an (S) (Expression)))

Unveiling The Internals

The attempt construct is just syntactic sugar for a function call.

attempt : conseq
else : alt

is equivalent to

with-attempt(
   fn () : conseq
   fn () : alt)

As an exercise, try and implement your own with-attempt function by using the label construct.

Exception Handling

Our s-expression parser from the previous example fails when called with invalid input (though with very nice error messages). Here is what happens if we forget a closing parenthesis at the end.

do(println, parse-sexp("This (is) (commonly (called an (S) (Expression))"))

Compiling and running the above prints out

FATAL ERROR: Unclosed opening parenthesis.
   at test.stanza:39.25
   at test.stanza:32.29
   at core/core.stanza:3725.13
   at core/core.stanza:847.16
   at core/core.stanza:3724.14
   ...

But what if we cannot guarantee that the input is correct? Suppose we want users to type an arbitrary string into the terminal and print the parsed s-expression if it's well formed, or else ask them to try again if it's not.

A potential solution would be to write another function called sexp? that returns true or false depending on whether its argument is a well formed string. But checking whether an s-expression is well formed is almost as much work as parsing it, so that's an inefficient solution.

Stanza provides us a mechanism for handling this called exceptions.

Exception Objects

The first step is to declare our own Exception types, one for each type of error the parser can encounter.

defstruct UnclosedParenthesis <: Exception
defmethod print (o:OutputStream, e:UnclosedParenthesis) :
   print(o, "Unclosed opening parenthesis.")
  
defstruct UnmatchedParenthesis <: Exception : (char:Char)
defmethod print (o:OutputStream, e:UnmatchedParenthesis) :
   print(o, "Expected closing parenthesis but got %~." % [char(e)])
  
defstruct UnexpectedCharacter <: Exception : (char:Char)
defmethod print (o:OutputStream, e:UnexpectedCharacter) :
   print(o, "Unexpected character: %~." % [char(e)])

There are three different errors that are detected by our parser. 

  1. The string is missing a closing parenthesis at the end.
  2. We are currently reading a list and encountered a strange character.
  3. We've read as many s-expressions as possible and there is a strange character left over.

Throwing Exceptions

The next step is to change the calls to fatal to calls to throw with our newly defined Exception objects.

defn parse-sexp (sexp:String) :

   ...
     
   defn parse-list () -> List :  
      ...
      if empty?(chars) : throw(UnclosedParenthesis())
      else if peek(chars) == ')' : (next(chars), items)
      else : throw(UnmatchedParenthesis(next(chars)))

   val items = parse-sequence()
   if empty?(chars) : items
   else : throw(UnexpectedCharacter(next(chars)))

Catching Exceptions

The final step is to catch the thrown exceptions. We can decide which types of exceptions to catch, and which not to. In this example, we'll assume that the string doesn't contain any strange characters and catch only the unclosed parenthesis error.

try :
   do(println, parse-sexp("This (is) (commonly (called an (S) (Expression))"))
catch (e:UnclosedParenthesis) :
   println("You forgot to close an opening parenthesis. Please try again.")

Compiling and running the program now prints out

You forgot to close an opening parenthesis. Please try again.

Here is the general form of the try construct.

try :
   body
catch (e:ExceptionA) :
   a-handler
catch (e:ExceptionB) :
   b-handler
...   

The try construct evaluates the given body after installing the given exception handlers. If body is evaluated without ever calling throw then its result is returned by try. If body calls throw with some Exception object, then try immediately searches for the first exception handler that can accept the Exception object and returns the result of evaluating that handler. 

Generators

The control flow constructs you've been introduced to so far, labeled scopes, attempts and failures, and exceptions, have all served the purpose of leaving a block of code. Generators are the first control flow construct you will learn capable of resuming a block of code. 

Here is a generator that yields the first three positive integers.

val xs:Seq<Int> = generate<Int> :
   println("Yielding One")
   yield(1)
   println("Yielding Two")
   yield(2)
   println("Yielding Three")
   yield(3)

Notice that the generate construct returns a Seq. Let's try printing out the items in the sequence.

println("The first item in xs is")
println(next(xs))
println("The second item in xs is")
println(next(xs))
println("The third item in xs is")
println(next(xs))

Compiling and running the above prints out

The first item in xs is
Yielding One
1
The second item in xs is
Yielding Two
2
The third item in xs is
Yielding Three
3

The Ability to Resume

It's worth paying attention to the order in which the messages are printed out. The snippet

println("The first item in xs is")
println(next(xs))

by itself, prints out

The first item in xs is
Yielding One
1

Thus the call to next(xs) causes control to enter the block of code in the generate construct. The message "Yielding One" is printed out, and then the call to yield(1) leaves the generate construct and 1 is the return value of next(xs).

The next snippet

println("The second item in xs is")
println(next(xs))

prints out

The second item in xs is
Yielding Two
2

Thus the call to next(xs) causes control to re-enter the block generate construct, resuming from just after the first call to yield. The message "Yielding Two" is printed out, and then the call to yield(2) leaves the generate construct once again and 2 is the return value of the second call to next(xs).

The last snippet

println("The third item in xs is")
println(next(xs))

prints out

The third item in xs is
Yielding Three
3

Similarly, the call to next(xs) resumes the block in the generate construct from just after the second call to yield. The message "Yielding Three" is printed out, and then the call to yield(3) leaves the generate construct once again and 3 is the return value of the third call to next(xs).

Thus the generate construct provides a very convenient way of creating a lazily constructed sequence. 

General Form

Here is the general form of the generate construct.

generate<T> :
   body

generate returns a Seq<T> by lazily executing the given body in a scope containing the generation functions, yield and break

yield is of type T -> False and its argument becomes an element in the generated Seq. Execution of the generate block pauses at yield, and is resumed on the next call to next on the sequence.

break is both of type () -> Void and T -> Void. If no argument is given to break, then execution of the generate block ends here and marks the end of the generated sequence. If an argument is given to break, then that element is first yielded before the generate block is ended.

If the generated type, T, is not explicitly provided, then it is assumed to be ? by default.

Example: Flattening a Tuple

In this example, we'll determine whether two tuples contain the same elements as each other if we lay out their elements in depth-first order. For example, the tuple

[[1] [2 [3]] [[4 5] 6]]

contains the elements 123456 once laid out in depth-first order.

Here's the most straightforward way of doing this. We'll write a function called flatten that returns a Vector containing a tuple's elements in depth-first order.

defn flatten (x:Tuple) -> Vector :
   val v = Vector<?>()
   defn loop (x) :
      match(x) :
         (x:Tuple) : do(loop, x)
         (x) : add(v, x)
   loop(x)        
   v      

Let's try it out.

println(flatten([[1] [2 [3]] [[4 5] 6]]))

Compiling and running the above prints out

[1 2 3 4 5 6]

To check whether two tuples contain the same elements, we can just flatten each of them and then compare the elements.

defn same-elements? (a:Tuple, b:Tuple) :
   if all?(equal?, flatten(a), flatten(b)) :
      println("%_ and %_ have the same elements." % [a, b])
   else :
      println("%_ and %_ have different elements." % [a, b])

Let's test it out on the following tuples.

same-elements?(
   [[1] [2 [3]] [[4 5] 6]]
   [1 [[2 3 4] [5]] [6]])

same-elements?(
   [[1] [2 [3]] [[4 5] 6]]
   [[[0] 2] [3 [4 5]] 6])

Compiling and running the above prints out

[[1] [2 [3]] [[4 5] 6]] and [1 [[2 3 4] [5]] [6]] have the same elements.
[[1] [2 [3]] [[4 5] 6]] and [[[0] 2] [3 [4 5]] 6] have different elements.

Notice though, that in both cases, we computed a full flattening of both tuples before checking to see whether they are equal. This is obviously inefficient in the second case since we can tell they are clearly different just by examining their first element. How do we avoid computing the full flattening?

The solution is to lazily compute the flattening. Let's change flatten to use the generate construct to lazily compute the flattened tuples. To track how much of the tuples are being flattened let's also add a print statement.

defn flatten (x:Tuple) -> Seq :
   val index = to-seq(0 to false)
   generate :
      defn loop (x) :
         match(x) :
            (x:Tuple) :
               do(loop, x)
            (x) :
               println("Yielding Item %_" % [next(index)])
               yield(x)
      loop(x)         

Compiling and running the program again prints out

Yielding Item 0
Yielding Item 0
Yielding Item 1
Yielding Item 1
Yielding Item 2
Yielding Item 2
Yielding Item 3
Yielding Item 3
Yielding Item 4
Yielding Item 4
Yielding Item 5
Yielding Item 5
[[1] [2 [3]] [[4 5] 6]] and [1 [[2 3 4] [5]] [6]] have the same elements.
Yielding Item 0
Yielding Item 0
[[1] [2 [3]] [[4 5] 6]] and [[[0] 2] [3 [4 5]] 6] have different elements.

Thus the results are the same as before, and you can see that, for the second comparison, both generators (one for each tuple) are only computing up to the first element.

Coroutines

The labelattempttry, and generate constructs are all specific usage patterns of Stanza's targetable coroutine system. Here we'll show you how to use the coroutine system in its full generality. It is rare in daily programming to encounter a problem that requires a use of coroutines that isn't already handled by one of the special case constructs. But for implementing libraries and frameworks that make heavy use of concurrency and non-standard control flow, coroutines may be indispensable. 

Sending Things Out

Here is the function that will represent our coroutine body.

defn my-process (co:Coroutine<Int,String>, a:Int) -> String :
   println("Passing out Timon")
   suspend(co, "Timon")
   println("Passing out and")
   suspend(co, "and")
   println("Passing out Pumbaa")
   suspend(co, "Pumbaa")
   println("Coroutine is done")
   "Done"

The type Coroutine<Int,String> represents a coroutine for which integers are sent into the coroutine, and for which strings are sent back from the coroutine. The function for sending values out of the coroutine is suspend

Let's now create our coroutine object and resume it a few times.

println("Create coroutine")
val co = Coroutine<Int,String>(my-process)

println("\nResume with 42")
val x = resume(co, 42)
println("Got back x = %_" % [x])

println("\nResume with 43")
val y = resume(co, 43)
println("Got back y = %_" % [y])

println("\nResume with 44")
val z = resume(co, 44)
println("Got back z = %_" % [z])

println("\nResume with 45")
val w = resume(co, 45)
println("Got back w = %_" % [w])

Notice that resume is called with integers. When the above is compiled and ran it prints out

Create coroutine

Resume with 42
Passing out Timon
Got back x = Timon

Resume with 43
Passing out and
Got back y = and

Resume with 44
Passing out Pumbaa
Got back z = Pumbaa

Resume with 45
Coroutine is done
Got back w = Done

Thus suspend acts much like yield did for the generate construct, and resume acts much like next did. This is no accident of course. The generate construct is implemented in terms of suspend and resume underneath.

Breaking Things Off

In addition to suspend, a function called break can also be used to send values out of a coroutine. The difference is that a call to break cannot later be resumed.

Let's change our my-process function to send out "Pumbaa" with break instead of suspend.

defn my-process (co:Coroutine<Int,String>, a:Int) -> String :
   println("Passing out Timon")
   suspend(co, "Timon")
   println("Passing out and")
   suspend(co, "and")
   println("Passing out Pumbaa")
   break(co, "Pumbaa")
   println("Coroutine is done")
   "Done"

Compiling and running the program again prints out

Create coroutine

Resume with 42
Passing out Timon
Got back x = Timon

Resume with 43
Passing out and
Got back y = and

Resume with 44
Passing out Pumbaa
Got back z = Pumbaa

Resume with 45
FATAL ERROR: Cannot resume coroutine. Coroutine is already closed.
   at core/core.stanza:984.13
   at core/core.stanza:862.16
   at core/core.stanza:897.40
   at core/core.stanza:862.16
   at test.stanza:31.8

The coroutine is closed after the call to break, and thus our call to resume fails.

Sending Things In

The obvious unanswered question now is: what is happening with the 424344, and 45 values that resume is being called with? To answer that, let's update our my-process function to print out the return values of suspend (and change the call to break back into suspend). 

defn my-process (co:Coroutine<Int,String>, a:Int) -> String :
   println("Came in a = %_" % [a])
   println("Passing out Timon")
   val b = suspend(co, "Timon")

   println("Came in b = %_" % [b])
   println("Passing out and")
   val c = suspend(co, "and")

   println("Came in c = %_" % [c])
   println("Passing out Pumbaa")
   val d = suspend(co, "Pumbaa")

   println("Came in d = %_" % [d])
   println("Coroutine is done")
   "Done"

println("Create coroutine")
val co = Coroutine<Int,String>(my-process)

println("\nResume with 42")
val x = resume(co, 42)
println("Got back x = %_" % [x])

println("\nResume with 43")
val y = resume(co, 43)
println("Got back y = %_" % [y])

println("\nResume with 44")
val z = resume(co, 44)
println("Got back z = %_" % [z])

println("\nResume with 45")
val w = resume(co, 45)
println("Got back w = %_" % [w])

Compiling and running the program again prints out

Create coroutine

Resume with 42
Came in a = 42
Passing out Timon
Got back x = Timon

Resume with 43
Came in b = 43
Passing out and
Got back y = and

Resume with 44
Came in c = 44
Passing out Pumbaa
Got back z = Pumbaa

Resume with 45
Came in d = 45
Coroutine is done
Got back w = Done

Thus, suspend sends its argument out from the coroutine, and returns the value sent into the coroutine. resume sends its argument into the coroutine, and returns the value sent out from the coroutine.

Closing Things Off

From outside the coroutine body, we may also choose to close a coroutine when we're finished with it. Let's try closing our coroutine after getting back "Pumbaa".

println("Create coroutine")
val co = Coroutine<Int,String>(my-process)

println("\nResume with 42")
val x = resume(co, 42)
println("Got back x = %_" % [x])

println("\nResume with 43")
val y = resume(co, 43)
println("Got back y = %_" % [y])

println("\nResume with 44")
val z = resume(co, 44)
println("Got back z = %_" % [z])

close(co)

println("\nResume with 45")
val w = resume(co, 45)
println("Got back w = %_" % [w])

Compiling and running the above prints out

Create coroutine

Resume with 42
Came in a = 42
Passing out Timon
Got back x = Timon

Resume with 43
Came in b = 43
Passing out and
Got back y = and

Resume with 44
Came in c = 44
Passing out Pumbaa
Got back z = Pumbaa

Resume with 45
FATAL ERROR: Cannot resume coroutine. Coroutine is already closed.
   at core/core.stanza:984.13
   at core/core.stanza:862.16
   at core/core.stanza:897.40
   at core/core.stanza:862.16
   at test.stanza:40.8

Checking a Coroutine's Status

There are two functions for checking on the status of a coroutine, active? and open?

Calling active? on a coroutine will return true if the coroutine's body is currently running, and false otherwise. Only active coroutines can be suspended or broken from.

Calling open? on a coroutine will return true if the coroutine's body is not currently running and open to be resumed. Only open coroutines can be resumed.

Nested Coroutines

A coroutine may also launch more coroutines. Notice that unlike yield, the calls to suspendbreak, and resume explicitly requires, as its first argument, the target coroutine. Being able to explicitly designate the target of the suspendbreak, and resume operations are key to allowing nested coroutines to work properly.

Consider the following code, where the coroutine, co1, launches a second coroutine, co2, within its body. Then within co2's body, there are suspend and break calls on both co1 and co2. Pay attention to how this interacts.

val co1 = Coroutine<False,Int> $ fn (co1, x0) :
   val co2 = Coroutine<False,False> $ fn (co2, y0) :
      for i in 0 to false do :
         suspend(co1, i)
         if i == 5 :
            println("Breaking from coroutine 2!")
            break(co2, false)
   resume(co2, false)
   -1

while open?(co1) :
   println(resume(co1, false))

Compiling and running the above prints out

0
1
2
3
4
5
Breaking from coroutine 2!
-1

The two nested coroutines are quite confusing. To get a better sense of what's happening, let's rewrite the second coroutine using the special case label construct.

val co1 = Coroutine<False,Int> $ fn (co1, x0) :
   label<False> break :
      for i in 0 to false do :
         suspend(co1, i)
         if i == 5 :
            println("Breaking from coroutine 2!")
            break(false)
   -1

while open?(co1) :
   println(resume(co1, false))

Compiling and running the above prints out the same message as before.

It is highly unlikely that you will feel the desire to directly launch new coroutines from within other coroutines, as we did here. But this example shows that they nest appropriately and generally do the right thing. Thus, for whatever abstractions you build on top of Stanza's targetable coroutine system, you can rest assured that they will recurse and compose correctly. 

Example: Key Listener

The following example demonstrates using coroutines to easily implement a key listener that translates individual key presses into events on strings.

Let us define a KeyListener type, and its fundamental operation.

deftype KeyListener
defmulti key-pressed (c:Char) -> False

KeyListener object listens to individual key presses from a keyboard and translates them into higher level events. Here is the definition of the constructor function for a KeyListener.

defn KeyListener (entered: String -> False) -> KeyListener

KeyListener takes a callback function called entered that accepts String objects. Our KeyListener translates key presses into calls to entered on space-separated words. It also supports deleting characters with the backspace key, entering of double-quoted strings, and escaped double-quotes within a double-quoted string. Here is specifically what it has to do.

  1. A KeyListener has an internal buffer for storing characters. When keys corresponding to letters are pressed, they are stored into the internal buffer.
  2. When the backspace key is pressed, the last character is deleted from the internal buffer.
  3. Once a full word is completed (indicated by the spacebar being pressed), the entered function should be called with the contents of the internal buffer.
  4. If the double quote key is pressed, then this indicates that a string is being started, and all subsequent characters until the next double quote should be stored in the internal buffer. Upon completion of the string, the entered function should be called with the entire contents of the internal buffer.
  5. During entering of a string, if a backslash character followed by a double quote character is entered, then the double quote character is stored in the internal buffer, and entering of the string continues. If the backslash character is not followed by a double quote then both characters are ignored.

Coroutine Framework

Here is the basic framework that we will use to ease programming the KeyListener.

defn KeyListener (entered: String -> False) -> KeyListener :
   val co = Coroutine<Char,False> $ fn (co, c0) :
      ;Retrieve the next character
      defn next-char () :
         suspend(co, false)

      ...

   new KeyListener :
      defmethod key-pressed (this, c:Char) :
         resume(co, c)

We immediately create a coroutine that accepts characters and sends back dummy values of type False. Within the coroutine body, the helper function next-char requests the next character by suspending the coroutine and returning the next character sent back into the coroutine. A new KeyListener object is returned that calls resume on the coroutine whenever a key is pressed.

Buffer Managing Routines

The following definitions within the coroutine body help us manage the KeyListener's internal buffer.

;Buffer commands
val buffer = Vector<Char>()
defn pop-char () :
   if not empty?(buffer) :
      pop(buffer)
defn add-char (c:Char) :
   add(buffer, c)
defn empty-buffer () :
   entered(string-join(buffer))
   clear(buffer)

The buffer is represented as a vector of characters. pop-char removes the last character in the buffer if possible. add-char adds the given character to the end of the buffer. empty-buffer calls the entered callback with the contents of the buffer, and then clears the buffer.

Dispatch Mode

The key press parser operates in a number of different modes. The default mode is the dispatch mode, which determines which mode to next enter based on the previously pressed key. 

;Dispatch mode
defn* parse (c:Char) :
   if letter?(c) :
      parse-word(c)
   else if c == '\"' :
      parse-string(next-char())
   else :
      parse(next-char())

If a letter key was pressed, then we start parsing a word event. If the double-quote key is pressed, then we start parsing a string event. Otherwise, the key press is ignored.

Word Mode

The word parsing mode accepts key presses until one word is completed.

;Word parsing mode
defn* parse-word (c:Char) :
   if letter?(c) :
      add-char(c)
      parse-word(next-char())
   else if c == '\b' :
      pop-char()
      parse-word(next-char())
   else if (c == ' ') or (c == '\"') :
      empty-buffer()
      parse(c)
   else :
      parse-word(next-char())

If the last key pressed was a letter, then that letter is added to the buffer. If the last key was a backspace, then we delete a character from the buffer. If the last key was the spacebar or the double-quote key, then the word is completed. We empty the buffer and then go back to the dispatch mode. All other keys are ignored.

String Mode

The string parsing mode accepts key presses until another (un-escaped) double-quote key finishes the string.

;String parsing mode
defn* parse-string (c:Char) :
   if c == '\"' :
      empty-buffer()
      parse(next-char())
   else if c == '\b' :
      pop-char()
      parse-string(next-char())
   else if c == '\\' :
      if next-char() == '\"' : add-char('\"')
      parse-string(next-char())
   else :
      add-char(c)
      parse-string(next-char())

If the last key pressed was the double-quote key then the string is completed. We empty the buffer and then go back to the dispatch mode. If the last key was a backspace, then we delete a character from the buffer. If the last key was the backslash key, then we add a double-quote to the buffer if the following key is a double-quote. Otherwise both keys are ignored. Finally, all other characters are added to the buffer.

Testing the KeyListener

Here is the entire KeyListener constructor function.

defn KeyListener (entered: String -> False) -> KeyListener :
   val co = Coroutine<Char,False> $ fn (co, c0) :
      ;Retrieve the next character
      defn next-char () :
         suspend(co, false)
        
      ;Buffer commands
      val buffer = Vector<Char>()
      defn pop-char () :
         if not empty?(buffer) :
            pop(buffer)
      defn add-char (c:Char) :
         add(buffer, c)
      defn empty-buffer () :
         entered(string-join(buffer))
         clear(buffer)

      ;Dispatch mode
      defn* parse (c:Char) :
         if letter?(c) :
            parse-word(c)
         else if c == '\"' :
            parse-string(next-char())
         else :
            parse(next-char())

      ;Word parsing mode
      defn* parse-word (c:Char) :
         if letter?(c) :
            add-char(c)
            parse-word(next-char())
         else if c == '\b' :
            pop-char()
            parse-word(next-char())
         else if (c == ' ') or (c == '\"') :
            empty-buffer()
            parse(c)
         else :
            parse-word(next-char())

      ;String parsing mode
      defn* parse-string (c:Char) :
         if c == '\"' :
            empty-buffer()
            parse(next-char())
         else if c == '\b' :
            pop-char()
            parse-string(next-char())
         else if c == '\\' :
            if next-char() == '\"' : add-char('\"')
            parse-string(next-char())
         else :
            add-char(c)
            parse-string(next-char())
           
      ;Launch!
      parse(c0)

   new KeyListener :
      defmethod key-pressed (this, c:Char) :
         resume(co, c)

Let's try it out on some simulated key presses.

defn keys-pressed (kl:KeyListener, cs:Seqable<Char>) :
   do(key-pressed{kl, _}, cs)

defn main () :
   val kl = KeyListener $ fn (s) :
      println("String entered: %_" % [s])
     
   ;Test backspace  
   keys-pressed(kl, "Timom\bn ")
  
   ;Test simple word
   keys-pressed(kl, "and ")

   ;Test backspace against empty buffer
   keys-pressed(kl, "P\b\b\b\bPumbaa")
  
   ;Test unrecognized characters
   keys-pressed(kl, " a#$!re")
  
   ;Test strings with escaped quotes
   keys-pressed(kl, \<S>"\"good\" friends!!"<S>)

main()

Note that 

\<S>literal !@#$%\|" characters<S>

is Stanza's syntax for a literal un-escaped string. All characters between the starting <S> tag and the ending <S> tag are part of the string. Any tag may be used in place of S.

Compiling and running the above prints out

String entered: Timon
String entered: and
String entered: Pumbaa
String entered: are
String entered: "good" friends!!

Our KeyListener calls the callback function at the correct times and with the correct input! 

The coroutine mechanism allowed us to keep the code fairly straightforward and modular, even though the logic behind the KeyListener itself is actually quite sophisticated. As an exercise, you may try to implement an equivalent KeyListener function without using the coroutine mechanism to fully appreciate how tedious and error-prone it is.