Rust’s generators are a game-changing feature that opens up new possibilities for creating stateful iterators. They’re like a secret weapon for crafting complex sequences with minimal memory usage. I’ve been exploring this powerful tool, and I’m excited to share what I’ve learned.
At its core, a generator is a function that can pause and resume execution, maintaining its local state between calls. This is incredibly useful for scenarios where you need to generate a sequence of values over time, without calculating everything upfront.
Let’s dive into a simple example to get a feel for how generators work in Rust:
#![feature(generators, generator_trait)]
use std::ops::{Generator, GeneratorState};
use std::pin::Pin;
fn main() {
let mut gen = || {
yield 1;
yield 2;
yield 3;
};
loop {
match Pin::new(&mut gen).resume(()) {
GeneratorState::Yielded(value) => println!("Generated value: {}", value),
GeneratorState::Complete(_) => break,
}
}
}
In this example, we create a generator that yields three values: 1, 2, and 3. We then use a loop to resume the generator repeatedly, printing each yielded value until it completes.
One of the coolest things about generators is how they allow us to create infinite sequences with ease. Here’s an example of a generator that produces the Fibonacci sequence:
#![feature(generators, generator_trait)]
use std::ops::{Generator, GeneratorState};
use std::pin::Pin;
fn main() {
let mut fibonacci = || {
let mut a = 0;
let mut b = 1;
loop {
yield a;
let temp = a + b;
a = b;
b = temp;
}
};
for _ in 0..10 {
match Pin::new(&mut fibonacci).resume(()) {
GeneratorState::Yielded(value) => println!("Fibonacci number: {}", value),
GeneratorState::Complete(_) => unreachable!(),
}
}
}
This generator will keep producing Fibonacci numbers indefinitely. We’re only printing the first 10 here, but you could keep going as long as you like.
One of the most powerful aspects of generators is their ability to maintain state between yields. This makes them perfect for modeling complex state machines or implementing coroutines.
Here’s an example of a more complex generator that simulates a simple game state:
#![feature(generators, generator_trait)]
use std::ops::{Generator, GeneratorState};
use std::pin::Pin;
enum GameState {
Start,
Playing,
GameOver,
}
fn main() {
let mut game = || {
let mut state = GameState::Start;
let mut score = 0;
loop {
match state {
GameState::Start => {
println!("Game starting!");
state = GameState::Playing;
yield "Start";
}
GameState::Playing => {
score += 1;
if score >= 3 {
state = GameState::GameOver;
}
yield "Playing";
}
GameState::GameOver => {
println!("Game over! Final score: {}", score);
return "GameOver";
}
}
}
};
loop {
match Pin::new(&mut game).resume(()) {
GeneratorState::Yielded(state) => println!("Current state: {}", state),
GeneratorState::Complete(final_state) => {
println!("Game ended with state: {}", final_state);
break;
}
}
}
}
This generator simulates a simple game with three states: Start, Playing, and GameOver. It maintains the current state and score between yields, allowing us to model a more complex system with minimal code.
Generators also excel at creating lazy evaluation pipelines. They allow you to describe complex sequences of operations without actually performing those operations until the values are needed. This can lead to significant performance improvements in certain scenarios.
Here’s an example of a lazy evaluation pipeline using generators:
#![feature(generators, generator_trait)]
use std::ops::{Generator, GeneratorState};
use std::pin::Pin;
fn main() {
let numbers = || {
for i in 0..100 {
yield i;
}
};
let squares = |mut input: Pin<&mut dyn Generator<Yield = i32, Return = ()>>| {
loop {
match input.as_mut().resume(()) {
GeneratorState::Yielded(x) => yield x * x,
GeneratorState::Complete(_) => return,
}
}
};
let mut pipeline = squares(Box::pin(numbers()));
for _ in 0..5 {
match Pin::new(&mut pipeline).resume(()) {
GeneratorState::Yielded(value) => println!("Squared value: {}", value),
GeneratorState::Complete(_) => break,
}
}
}
In this example, we create a pipeline that generates numbers from 0 to 99, then squares them. However, thanks to the lazy evaluation provided by generators, we only compute the first 5 squared values, even though our number generator is capable of producing 100 values.
One area where generators really shine is in implementing custom iterators. They allow you to write iterator-like code in a much more intuitive way, without having to implement the full Iterator trait manually.
Here’s an example of a custom iterator implemented using a generator:
#![feature(generators, generator_trait)]
use std::ops::{Generator, GeneratorState};
use std::pin::Pin;
struct Iter<G>(G);
impl<G> Iterator for Iter<G>
where
G: Generator<Yield = i32, Return = ()> + Unpin,
{
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
match Pin::new(&mut self.0).resume(()) {
GeneratorState::Yielded(value) => Some(value),
GeneratorState::Complete(_) => None,
}
}
}
fn even_numbers() -> impl Iterator<Item = i32> {
Iter(|| {
let mut n = 0;
loop {
yield n;
n += 2;
}
})
}
fn main() {
for n in even_numbers().take(5) {
println!("Even number: {}", n);
}
}
This example creates an iterator that generates even numbers using a generator. The Iter struct wraps a generator and implements the Iterator trait, allowing us to use it with Rust’s standard iterator methods like take().
Generators can also be incredibly useful for dealing with asynchronous code. While Rust has async/await syntax for handling futures, generators can provide a lower-level way to work with asynchronous operations.
Here’s a simple example of using a generator for asynchronous operations:
#![feature(generators, generator_trait)]
use std::ops::{Generator, GeneratorState};
use std::pin::Pin;
use std::time::{Duration, Instant};
use std::thread;
fn main() {
let mut async_operation = || {
let start = Instant::now();
yield "Operation started";
thread::sleep(Duration::from_secs(2));
yield "Operation in progress";
thread::sleep(Duration::from_secs(1));
yield format!("Operation completed in {:?}", start.elapsed());
};
loop {
match Pin::new(&mut async_operation).resume(()) {
GeneratorState::Yielded(status) => println!("Status: {}", status),
GeneratorState::Complete(_) => break,
}
}
}
This generator simulates an asynchronous operation that takes place over multiple steps, yielding status updates along the way.
As powerful as generators are, it’s important to note that they’re still an unstable feature in Rust. This means you’ll need to use a nightly compiler and enable the feature explicitly in your code. However, the core team is working on stabilizing generators, and they’re likely to become a standard part of Rust in the future.
In conclusion, Rust’s generators are a powerful tool for creating stateful iterators and handling complex sequences. They allow us to write more expressive and efficient code, especially in scenarios involving intricate iteration patterns or state management. By mastering generators, we can push the boundaries of what’s possible with Rust’s iterator ecosystem while still maintaining its promise of zero-cost abstractions.
Whether you’re implementing coroutines, creating lazy evaluation pipelines, or modeling complex state machines, generators provide a flexible and intuitive way to structure your code. As Rust continues to evolve, I expect generators to become an increasingly important part of the language, opening up new possibilities for elegant and efficient programming.