【问题标题】:Implement Haskell-like primes iterator in Rust在 Rust 中实现类似 Haskell 的素数迭代器
【发布时间】:2022-01-08 03:40:27
【问题描述】:

我正在尝试实现类似于以下 Haskell 代码的 Rust 迭代器:

primes :: [Integer]
primes = nextPrime [2..]
    where 
        nextPrime (x:xs) = x : nextPrime (filter (notDivBy x) xs)
        notDivBy a x = a `mod` x /= 0

到目前为止我的尝试 (playground):

// as pointed out in a comment this can simply return Primes and not use Box::new
fn primes() -> Box<dyn Iterator<Item = usize>> {
    Box::new(Primes::new())
}

struct Primes {
    nums: Box<dyn Iterator<Item = usize>>,
}

impl Primes {
    fn new() -> Self {
        Primes { nums : Box::new(2..) }
    }
}

impl Iterator for Primes {
    type Item = usize;
    
    fn next(&mut self) -> Option<usize> {
        
        let prime = self.nums.next().unwrap();
        self.nums = Box::new(self.nums.filter(move |&n|!divides(prime, n)));
        //use std::borrow::BorrowMut;
        //*self.nums.borrow_mut() = self.nums.filter(move |&n|!divides(prime, n));
        Some(prime)
    }
}


pub fn divides(d: usize, n: usize) -> bool {
    n % d == 0
}

不幸的是,这遇到了:

error[E0507]: cannot move out of `self.nums` which is behind a mutable reference
  --> src/lib.rs:22:30
   |
22 |         self.nums = Box::new(self.nums.filter(move |&n| !divides(prime, n)));
   |                              ^^^^^^^^^ move occurs because `self.nums` has type `Box<dyn Iterator<Item = usize>>`, which does not implement the `Copy` trait

For more information about this error, try `rustc --explain E0507`.

或者,如果您取消注释替代的 borrow_mut 代码:

error[E0277]: the trait bound `Box<(dyn Iterator<Item = usize> + 'static)>: BorrowMut<Filter<Box<dyn Iterator<Item = usize>>, [closure@src/lib.rs:24:52: 24:79]>>` is not satisfied
  --> src/lib.rs:24:20
   |
24 |         *self.nums.borrow_mut() = self.nums.filter(move |&n|!divides(prime, n));
   |                    ^^^^^^^^^^ the trait `BorrowMut<Filter<Box<dyn Iterator<Item = usize>>, [closure@src/lib.rs:24:52: 24:79]>>` is not implemented for `Box<(dyn Iterator<Item = usize> + 'static)>`
   |
   = help: the following implementations were found:
             <Box<T, A> as BorrowMut<T>>

坦率地说,我什至不确定这两个中哪一个更接近工作。

【问题讨论】:

  • 你对拳击的使用在这里很奇怪。为什么primes() 不只是返回Primes?为什么将(装箱!)迭代器存储到一个可以计数的东西而不是直接存储一个计数器?我认为如果你的目标是减少动态分配,它会简单得多。
  • @GManNickG,最初我没有这个拳击,但是建立过滤器链让编译器抱怨无法确定迭代器的大小,这促使我引入拳击。 IIRC。也许我对 Box 的使用有点过头了?
  • @GManNickG,我现在在代码中添加了一条评论。谢谢指点!

标签: haskell rust iterator primes


【解决方案1】:

我认为你可以缓存发现的素数:

fn primes() -> Primes {
    Primes::new()
}

struct Primes {
    nums: Box<dyn Iterator<Item = usize>>,
    cache: Vec<usize>,
}

impl Primes {
    fn new() -> Self {
        Primes {
            nums: Box::new(2..),
            cache: Vec::new(),
        }
    }
}

impl Iterator for Primes {
    type Item = usize;

    fn next(&mut self) -> Option<usize> {
        loop {
            let num = self.nums.next().unwrap();
            if self
                .cache
                .iter()
                .take_while(|&&n| n*n <= num)
                .all(|&p| !divides(p, num))
            {
                // we found a prime!
                self.cache.push(num);
                return Some(num);
            }
        }
    }
}

pub fn divides(d: usize, n: usize) -> bool {
    n % d == 0
}

fn main() {
    println!("Primes: {:?}", primes().take(10).collect::<Vec<_>>());
}

Playground

【讨论】:

  • 有趣!使用缓存,您更接近于模拟 Haskell 语义。
  • @hkBst 实际上,我相信这使用了比原始 Haskell 代码更有效的技术,因为这最终会构建一个大的过滤器链filter p1 (filter p2 (...,而不是保留一个缓存并且只应用一个筛选。我认为您可以修改您的 Haskell 以使用 Set 作为缓存并用一些 unfoldr 替换过滤器。
  • @chi,Haskell 代码建立了一个素数列表,但你说得对,它也许可以更有效地过滤。
  • 它应该是take_while(|&amp;&amp;n| !(n*n &gt; num))。否则,素数的平方就会通过。
  • 当然不是。 =
【解决方案2】:

我并不是真正的 Rust 专家,但一个(丑陋的)选择是将 self.nums 替换为虚拟迭代器,以便能够移动以前的值。

fn next(&mut self) -> Option<usize> {
    let prime = self.nums.next().unwrap();
    // Ugly replacement with a dummy iterator value
    let rest = std::mem::replace(&mut self.nums, Box::new(0..));
    self.nums = Box::new(rest.filter(move |&n|!divides(prime, n)));
    Some(prime)
}

如果我们使用 Option 包装器,这可能会更简洁,以便我们可以使用 None 作为虚拟值。

【讨论】:

  • 它有效,但我不明白为什么。 :D
  • 这个 iiuc 的问题是你在每次下一次通话时都要重新装箱。所以到第 10 个素数时,您将已经有一个 box(box(box(box...))) 包装间接。
  • @Netwave 确实,它效率不高。但这是对原始 Haskell 代码中发生的事情的“愚蠢”翻译,我们得到一串过滤器,每个过滤器都指向另一个。运行该 Haskell 代码,我们确实得到了越来越大的间接链。有效的替代方法是缓存素数,就像你做的那样——这适用于两种语言。
  • @hkBst 假设我们希望与原始 Haskell 紧密匹配,您的问题是您正在尝试(大致)执行x = f(x),其中f 消耗(移动)其输入f(x:T)-&gt;T。当我们拥有 x 时,这不是问题,但当我们只有一个 mut 引用 y 时,这会成为问题,因为我们无法摆脱它——我们不拥有数据。充其量,我们可以replace 使用虚拟值的数据,执行let x = replace(y, dummy)。在那之后,我们拥有x,所以我们可以用*y = f(x)结束,覆盖虚拟值。
  • postponing 创建每个过滤器直到其素数的平方与使用一个通过缓存素数的统一测试一样有效(大概只到 sqrt)。
【解决方案3】:

我终于想通了(在其他答案的帮助下)如何解决无法移出可变引用背后的self.nums。诀窍是使用 &amp;mut self.nums(并摆脱 Box)。

fn primes() -> Primes {
    Primes::new()
}

pub struct Primes {
    nums: std::ops::RangeFrom<usize>,
    cache: Vec<usize>,
}

impl Primes {
    fn new() -> Self {
        Primes {
            nums: 2..,
            cache: Vec::new(),
        }
    }
}

impl Iterator for Primes {
    type Item = usize;

    fn next(&mut self) -> Option<usize> {
        let next_prime = (&mut self.nums)
            .filter(|&n| {
                self.cache
                    .iter()
                    .take_while(|&p| p * p <= n)
                    .all(|&p| !divides(p, n))
            })
            .next()
            .unwrap();
        self.cache.push(next_prime);
        return Some(next_prime);
    }
}

pub fn divides(d: usize, n: usize) -> bool {
    n % d == 0
}

fn main() {
    println!("Primes: {:?}", primes().take(10).collect::<Vec<_>>());
}

playground

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-04-25
    • 1970-01-01
    • 1970-01-01
    • 2021-08-02
    • 1970-01-01
    • 2023-03-31
    • 1970-01-01
    • 2011-05-11
    相关资源
    最近更新 更多