Rust中的惰性序列生成

Lazy sequence generation in Rust

如何创建其他语言称为惰性序列或"生成器"函数的语言?

在Python中,如下面的示例(来自Python的文档)所示,我可以使用yield来懒惰地生成一个可迭代的序列,该序列以不使用中间列表的内存的方式进行迭代:

1
2
3
4
5
6
7
8
# a generator that yields items instead of returning a list
def firstn(n):
    num = 0
    while num < n:
        yield num
        num += 1

sum_of_first_n = sum(firstn(1000000))

如何在Rust中做类似的事情?


Rust确实有生成器,但是它们是高度实验性的,目前尚无法在稳定的Rust中使用。

在稳定的Rust 1.0及更高版本中工作

Range处理您的具体示例。您可以将其与..的语法糖一起使用:

1
2
3
4
fn main() {
    let sum: u64 = (0..1_000_000).sum();
    println!("{}", sum)
}

如果Range不存在怎么办?我们可以创建一个对它进行建模的迭代器!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct MyRange {
    start: u64,
    end: u64,
}

impl MyRange {
    fn new(start: u64, end: u64) -> MyRange {
        MyRange {
            start: start,
            end: end,
        }
    }
}

impl Iterator for MyRange {
    type Item = u64;

    fn next(&mut self) -> Option<u64> {
        if self.start == self.end {
            None
        } else {
            let result = Some(self.start);
            self.start += 1;
            result
        }
    }
}

fn main() {
    let sum: u64 = MyRange::new(0, 1_000_000).sum();
    println!("{}", sum)
}

胆量是相同的,但是比Python版本更明确。值得注意的是,Python的生成器会为您跟踪状态。 Rust更喜欢显式,因此我们必须创建自己的状态并手动更新。重要的部分是Iterator特征的实现。我们指定迭代器产生特定类型(type Item = u64)的值,然后处理逐步执行每次迭代以及如何告知我们已到达迭代结束。

该示例不如使用泛型的真正Range强大,但显示了如何执行此操作的示例。

可在每晚Rust中使用

每晚Rust都有发电机,但是它们是高度实验性的。您需要引入一些不稳定的功能才能创建一个。但是,它看起来非常类似于Python示例,其中包含一些Rust特定的附加功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1.43.0-nightly (2020-02-09 71c7e149e42cb0fc78a8)
#![feature(generators, generator_trait)]

use std::{
    ops::{Generator, GeneratorState},
    pin::Pin,
};

fn firstn(n: u64) -> impl Generator<Yield = u64, Return = ()> {
    move || {
        let mut num = 0;
        while num < n {
            yield num;
            num += 1;
        }
    }
}

由于当前Rust中的所有内容都在迭代器上运行,因此我们创建了一个适配器,可将生成器转换为迭代器,以便与更广泛的生态系统一起使用。我希望这样的适配器最终会出现在标准库中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct GeneratorIteratorAdapter<G>(Pin<Box<G>>);

impl<G> GeneratorIteratorAdapter<G>
where
    G: Generator<Return = ()>,
{
    fn new(gen: G) -> Self {
        Self(Box::pin(gen))
    }
}

impl<G> Iterator for GeneratorIteratorAdapter<G>
where
    G: Generator<Return = ()>,
{
    type Item = G::Yield;

    fn next(&mut self) -> Option<Self::Item> {
        match self.0.as_mut().resume(()) {
            GeneratorState::Yielded(x) => Some(x),
            GeneratorState::Complete(_) => None,
        }
    }
}

现在我们可以使用它:

1
2
3
4
5
fn main() {
    let generator_iterator = GeneratorIteratorAdapter::new(firstn(1_000_000));
    let sum: u64 = generator_iterator.sum();
    println!("{}", sum);
}

有趣的是,它没有Iterator的实现那么强大。例如,迭代器具有size_hint方法,该方法使迭代器的使用者可以了解剩余多少个元素。 collect放入容器时,可以进行优化。生成器没有任何此类信息。


Rust 1.0没有生成器功能,因此您必须使用显式迭代器手动进行操作。

首先,使用next()方法将您的Python示例重写为一个类,因为它更接近您可能在Rust中获得的模型。然后,您可以使用实现Iterator特征的结构在Rust中重写它。

您也许还可以使用返回闭包的函数来实现类似的结果,但我认为不可能实现Iterator特征(因为将要求调用它来生成新结果) )。


从Rust 1.34稳定版开始,您具有方便的std::iter::from_fn实用程序。它不是协程(即您每次仍然必须返回),但是至少它使您不必定义其他结构。

from_fn接受闭包FnMut() -> Option< T >,并反复调用它以创建Iterator< T >

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// -> Box<dyn std::iter::Iterator<Item=u64>> in Rust 2015
fn firstn(n: u64) -> impl std::iter::Iterator<Item = u64> {
    let mut num = 0;
    std::iter::from_fn(move || {
        let result;
        if num < n {
            result = Some(num);
            num += 1
        } else {
            result = None
        }
        result
    })
}

fn main() {
  let sum_of_first_n = firstn(1000000).sum::<u64>();
  println!("sum(0 to 999999): {}", sum_of_first_n);
}

std::iter::successors也可用。它不那么通用,但可能会更容易使用,因为您只需显式传递种子值即可。 (即,它需要一个初始值T和一个后继函数FnMut(&T) -> Option< T >来创建一个Iterator< T >)

1
2
3
4
5
6
7
8
9
10
11
12
fn firstn(n: u64) -> impl std::iter::Iterator<Item = u64> {
    std::iter::successors(
        Some(0),
        move |&num| {
            if num + 1 < n {
                Some(num + 1)
            } else {
                None
            }
        },
    )
}

但是,Shepmaster的注释也适用于这些实用程序。 (tldr:通常,手动滚动Iterator的内存效率更高)

What's interesting about this is that it's less powerful than an implementation of Iterator. For example, iterators have the size_hint method, which allows consumers of the iterator to have an idea of how many elements are remaining. This allows optimizations when collecting into a container. Generators do not have any such information.

(注意:返回impl是Rust 2018的功能。有关详细信息,请参见《版本指南》)


您可以使用支持稳定Rust的堆栈式Rust生成器库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#[macro_use]
extern crate generator;
use generator::{Generator, Gn};

fn firstn(n: usize) -> Generator<'static, (), usize> {
    Gn::new_scoped(move |mut s| {
        let mut num = 0;
        while num < n {
            s.yield_(num);
            num += 1;
        }
        done!();
    })
}

fn main() {
    let sum_of_first_n: usize = firstn(1000000).sum();
    println!("sum ={}", sum_of_first_n);
}

或更简单地说:

1
2
3
4
5
6
7
8
9
10
11
let n = 100000;
let range = Gn::new_scoped(move |mut s| {
    let mut num = 0;
    while num < n {
        s.yield_(num);
        num += 1;
    }
    done!();
});

let sum: usize = range.sum();