Coroutines

Table of contents

  1. The basic concept
  2. Generators
  3. Sending data into a coroutine

The basic concept

A coroutine is a function that can be stopped at some state and resumed later. At its stop it yields a value.

An easy example:

g = || fn*||
   yield 1
   yield 2
   yield 1
   yield 4
end

This is used as follows:

> i = g()
> i(), i(), i(), i(), i()
[1, 2, 1, 4, empty]

> g().list()
[1, 2, 1, 4]

Generators

The following is a simple prime number test:

isprime = |n| (1..n).count(|k| n%k==0)==2

We can make it a little bit faster:

isprime = |n| n>1 and (2..).until(|k| k*k>n).all(|k| n%k!=0)

Now we want to state a function that filters elements from an iterable object by a given predicate p.

Iterable.filter = fn|a;p|
   y = []
   for x in a
      if p(x)
         y.push(x)
      end
   end
   return y
end

Let us test it out:

> (1..20).filter(isprime)
[2, 3, 5, 7, 11, 13, 17, 19]

But what if we want to calculate the first 10 prime numbers? One could provide a test range with upper bound, large enough, and take 10 of them afterwards:

> (1..100).filter(isprime).list(10)
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

But the more primes we want, the larger this upper bound has to be. The problem is, that we must know how large it is in before. For prime numbers there are mathematical results. To be more precise, we would need to invert the prime number counting function π(n). This means we want to solve the equation π(n)=10. A known mathematical result in case of n≥17 is

n/ln(n) < π(n).

By transitivity of inequalities we have

a < π(n) and 10 ≤ a implies 10 < π(n)

for all a. We place in a:=n/ln(n) and obtain

10 ≤ n/ln(n) implies 10 < π(n).

The first such n is 36 with π(36)=11.

This was somewhat complicated, and there are mathematical problems that are much more complicated. In general, we simply don't know how large such a range has to be.

The solution is, not to know the range in before. In order to do that, we use a coroutine.

Iterable.filter = |a;p| fn*||
   for x in a
      if p(x)
         yield x
      end
   end
end

Let us test it out:

> (1..).filter(isprime).list(10)
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

The prime numbers will be printed instantly:

for p in (1..).filter(isprime)
  print(p)
end

Or for convenience:

(1..).filter(isprime).each(print)

Sending data into a coroutine

A coroutine, that we can send data to, takes an additional argument. If this argument x is made optional, the coroutine may be used like an ordinary generator:

g = |a,b| fn*|x=null|
   for k in a..b
      yield [x,k]
   end
end

g(1,4).each(print)

# Output:
# [null, 1]
# [null, 2]
# [null, 3]
# [null, 4]

Now, let us send data into it:

i = g(1,2)
print(i("a"))
print(i("b"))
print(i("c"))
print(i("d"))

# Output:
# ["a", 1]
# ["b", 2]
# empty
# empty

It may be used this way:

i = g(1,4)
for x in "a".."z"
   y = i(x)
   if y==empty then break end
   print(y)
end

Or more elaborately:

for x in ("a".."z").map(g(1,4))
   print(x)
end

# Output:
# ["a", 1]
# ["b", 2]
# ["c", 3]
# ["d", 4]

Reaching the upper bound of the range fed into coroutine g(1,n) implicates the iterator to be exhausted too:

for x in ("a".."d").map(g(1,100))
   print(x)
end

# Output:
# ["a", 1]
# ["b", 2]
# ["c", 3]
# ["d", 4]