Generators
Generators are a convenient method for constructing functions which are capable of suspending themselves and their state.
The simplest use case for generators is to create a kind of iterator, whose state is stored in the generator function.
With this, we can create a fairly efficient generator to build fibonacci numbers.
fn fib() {
let a = 0;
let b = 1;
loop {
yield a;
let c = a + b;
a = b;
b = c;
}
}
let g = fib();
while let Some(n) = g.next() {
println!("{n}");
if n > 100 {
break;
}
}
$> cargo run -- run scripts/book/generators/fib_generator.rn
0
1
1
2
3
5
8
13
21
34
55
89
144
Advanced generators with GeneratorState
Generators internally are a bit more complex than that.
The next
function simply slates over some of that complexity to make simple
things easier to do.
The first thing to know is that yield
itself can actually produce a value,
allowing the calling procedure to send values to the generator.
fn printer() {
let collected = [];
for _ in 0..2 {
let out = yield;
println!("{:?}", out);
collected.push(out);
}
assert_eq!(collected, ["John", (1, 2, 3)]);
}
let printer = printer();
printer.resume(());
printer.resume("John");
printer.resume((1, 2, 3));
$> cargo run -- run scripts/book/generators/send_values.rn
"John"
(1, 2, 3)
But wait, what happened to the first value we sent, 1
?
Well, generators don't run immediately, they need to be "warmed up" by calling resume once. At that point it runs the block prior to the first yield, we can see this by instrumenting our code a little.
fn printer() {
loop {
println!("waiting for value...");
let out = yield;
println!("{out:?}");
}
}
let printer = printer();
println!("firing off the printer...");
printer.resume(());
println!("ready to go!");
printer.resume("John");
printer.resume((1, 2, 3));
$> cargo run -- run scripts/book/generators/bootup.rn
firing off the printer...
waiting for value...
ready to go!
"John"
waiting for value...
(1, 2, 3)
waiting for value...
Ok, so we understand how to send values into a generator. But how do we receive them?
This adds a bit of complexity, since we need to pull out GeneratorState
.
This enum has two variants: Yielded
and Complete
, and represents all the
possible states a generator can suspend itself into.
fn print_once() {
let out = yield 1;
println!("{:?}", out);
2
}
let printer = print_once();
dbg!(printer.resume(()));
dbg!(printer.resume("John"));
$> cargo run -- run scripts/book/generators/states.rn
Yielded(1)
"John"
Complete(2)
After the first call to resume, we see that the generator produced Yielded(1)
.
This corresponds to the yield 1
statement in the generator.
The second value we get is Complete(2)
.
This corresponds to the return value of the generator.
Trying to resume the generator after this will cause the virtual machine to error.
fn print_once() {
yield 1
}
let printer = print_once();
dbg!(printer);
dbg!(printer.resume(()));
dbg!(printer.resume("John"));
dbg!(printer);
dbg!(printer.resume(()));
$> cargo run -- run scripts/book/generators/error.rn
Generator { completed: false }
Yielded(1)
Complete("John")
Generator { completed: true }
error: virtual machine error
┌─ scripts/book/generators/error.rn:11:9
│
11 │ dbg!(printer.resume(()));
│ ^^^^^^^^^^^^^^^^^^ cannot resume a generator that has completed