在Rust

时间:2016-03-23 01:56:30

标签: rust

我正在尝试从F#移植一点基准到Rust。 F#代码如下所示:

let inline iterNeighbors f (i, j) =
  f (i-1, j)
  f (i+1, j)
  f (i, j-1)
  f (i, j+1)

let rec nthLoop n (s1: HashSet<_>) (s2: HashSet<_>) =
  match n with
  | 0 -> s1
  | n ->
      let s0 = HashSet(HashIdentity.Structural)
      let add p =
        if not(s1.Contains p || s2.Contains p) then
          ignore(s0.Add p)
      Seq.iter (fun p -> iterNeighbors add p) s1
      nthLoop (n-1) s0 s1

let nth n p =
  nthLoop n (HashSet([p], HashIdentity.Structural)) (HashSet(HashIdentity.Structural))

(nth 2000 (0, 0)).Count

它从潜在无限图中的初始顶点计算第n个最近邻壳。我在博士期间使用类似的东西研究无定形材料。

我花了很多时间尝试并且没有将它移植到Rust。我设法让一个版本工作,但只能通过手动内联闭包并将递归转换为带有本地变量的循环(yuk!)。

我尝试像这样编写iterNeighbors函数:

use std::collections::HashSet;

fn iterNeighbors<F>(f: &F, (i, j): (i32, i32)) -> ()
where
    F: Fn((i32, i32)) -> (),
{
    f((i - 1, j));
    f((i + 1, j));
    f((i, j - 1));
    f((i, j + 1));
}

我认为这是一个接受闭包的函数(它本身接受一对并返回单位)和一对并返回单位。我似乎必须加倍支架:这是正确的吗?

我尝试编写这样的递归版本:

fn nthLoop(n: i32, s1: HashSet<(i32, i32)>, s2: HashSet<(i32, i32)>) -> HashSet<(i32, i32)> {
    if n == 0 {
        return &s1;
    } else {
        let mut s0 = HashSet::new();
        for &p in s1 {
            if !(s1.contains(&p) || s2.contains(&p)) {
                s0.insert(p);
            }
        }
        return &nthLoop(n - 1, s0, s1);
    }
}

请注意,我还没有打扰iterNeighbors的电话。

我认为我正在努力让参数的生命周期正确,因为它们在递归调用中被旋转。如果我希望在s2之前释放return并且我希望s1在返回时或在递归调用中生存时,我应该如何注释生命周期?

来电者看起来像这样:

fn nth<'a>(n: i32, p: (i32, i32)) -> &'a HashSet<(i32, i32)> {
    let s0 = HashSet::new();
    let mut s1 = HashSet::new();
    s1.insert(p);
    return &nthLoop(n, &s1, s0);
}

我放弃了,并将其写成while循环,改为使用可变的本地人:

fn nth<'a>(n: i32, p: (i32, i32)) -> HashSet<(i32, i32)> {
    let mut n = n;
    let mut s0 = HashSet::new();
    let mut s1 = HashSet::new();
    let mut s2 = HashSet::new();
    s1.insert(p);
    while n > 0 {
        for &p in &s1 {
            let add = &|p| {
                if !(s1.contains(&p) || s2.contains(&p)) {
                    s0.insert(p);
                }
            };
            iterNeighbors(&add, p);
        }
        std::mem::swap(&mut s0, &mut s1);
        std::mem::swap(&mut s0, &mut s2);
        s0.clear();
        n -= 1;
    }
    return s1;
}

如果我手动内联闭包,这是有效的,但我无法弄清楚如何调用闭包。理想情况下,我想在这里静态发送。

main函数是:

fn main() {
    let s = nth(2000, (0, 0));
    println!("{}", s.len());
}

那么......我做错了什么? : - )

另外,我只在F#中使用HashSet,因为我认为Rust不提供具有有效集合理论操作(并集,交集和差异)的纯函数Set。我是否正确地假设?

2 个答案:

答案 0 :(得分:8)

  

我似乎必须加倍支架:这是正确的吗?

否:双bracketes是因为你选择使用元组并调用一个带元组的函数需要先创建元组,但是可以有一个带有多个参数的闭包,比如F: Fn(i32, i32)。也就是说,可以将该函数写为:

fn iterNeighbors<F>(i: i32, j: i32, f: F)
where
    F: Fn(i32, i32),
{
    f(i - 1, j);
    f(i + 1, j);
    f(i, j - 1);
    f(i, j + 1);
}

然而,似乎保留元组对于这种情况是有意义的。

  

我认为我正在努力让参数的生命周期正确,因为它们在递归调用中被旋转。如果我希望s2在返回之前被释放并且我希望s1在返回时或者在递归调用中生存时,我应该如何注释生命周期?

无需引用(因此不需要生命周期),只需直接传递数据:

fn nthLoop(n: i32, s1: HashSet<(i32, i32)>, s2: HashSet<(i32, i32)>) -> HashSet<(i32, i32)> {
    if n == 0 {
        return s1;
    } else {
        let mut s0 = HashSet::new();
        for &p in &s1 {
            iterNeighbors(p, |p| {
                if !(s1.contains(&p) || s2.contains(&p)) {
                    s0.insert(p);
                }
            })
        }
        drop(s2); // guarantees timely deallocation
        return nthLoop(n - 1, s0, s1);
    }
}

这里的关键是你可以按价值做所有事情,价值传递的东西当然会保持价值。

然而,这无法编译:

error[E0387]: cannot borrow data mutably in a captured outer variable in an `Fn` closure
  --> src/main.rs:21:21
   |
21 |                     s0.insert(p);
   |                     ^^
   |
help: consider changing this closure to take self by mutable reference
  --> src/main.rs:19:30
   |
19 |               iterNeighbors(p, |p| {
   |  ______________________________^
20 | |                 if !(s1.contains(&p) || s2.contains(&p)) {
21 | |                     s0.insert(p);
22 | |                 }
23 | |             })
   | |_____________^

也就是说,闭包试图改变它捕获的值(s0),但Fn闭包特征不允许这样做。可以以更灵活的方式(共享时)调用该特征,但是这会对封闭在内部执行的操作施加更多限制。 (如果您有兴趣,I've written more about this

幸运的是,有一个简单的解决方法:使用FnMut特征,这要求只有在对其具有唯一访问权限时才能调用闭包,但允许内部变异。

fn iterNeighbors<F>((i, j): (i32, i32), mut f: F)
where
    F: FnMut((i32, i32)),
{
    f((i - 1, j));
    f((i + 1, j));
    f((i, j - 1));
    f((i, j + 1));
}
  

来电者看起来像这样:

值也适用于此:在这种情况下返回引用将返回一个指向s0的指针,该指针生成在函数返回时被销毁的堆栈帧。也就是说,引用指向死数据。

修复程序只是没有使用引用:

fn nth(n: i32, p: (i32, i32)) -> HashSet<(i32, i32)> {
    let s0 = HashSet::new();
    let mut s1 = HashSet::new();
    s1.insert(p);
    return nthLoop(n, s1, s0);
}
  

如果我手动内联闭包,但是我无法弄清楚如何调用闭包,这是有效的。理想情况下,我想在这里静态发送。

(我不明白这意味着什么,包括你遇到麻烦的编译错误信息可以帮助我们。)

  

另外,我只在F#中使用HashSet,因为我认为Rust不提供具有有效集合理论操作(并集,交集和差异)的纯函数集。我是否正确地假设?

根据您的具体要求,不,例如, HashSetBTreeSet都提供了methods which return iterators的各种集合论操作。

一些小问题:

  • 显式/命名生命周期允许编译器推断数据的静态有效性,他们不控制它(即它们允许编译器在你做错的时候指出,但是语言仍然具有相同类型的静态资源用法/生命周期保证为C ++)
  • 带有循环的版本可能在写入时更有效,因为它直接重用内存(交换集合,加上s0.clear(),但是,通过传递{递归版本可以实现相同的好处{1}}重新使用而不是丢弃它。
  • s2循环可以是while
  • 不需要通过引用传递闭包,但无论有没有引用,仍然有静态分派(闭包是一个类型参数,而不是特征对象)。
  • 传统上,闭包参数是最后的,而不是引用,因为它使得定义&amp;将内联传递更易于阅读(例如for _ in 0..n而不是foo(x, |y| bar(y + 1))
  • 跟踪返回时不需要foo(&|y| bar(y + 1), x)关键字(如果省略return):

    ;

答案 1 :(得分:8)

  

我认为这是一个接受闭包的函数(它本身接受一对并返回单位)和一对并返回单位。我似乎必须加倍支架:这是正确的吗?

你需要双括号,因为你将2元组传递给闭包,它与你原来的F#代码相匹配。

  

我认为我努力让参数的生命周期正确,因为它们在递归调用中被旋转。如果我希望s2在返回之前被释放并且我希望s1在返回时或者在递归调用中生存时,我应该如何注释生命周期?

问题是,当您直接使用HashSet时,您正在使用对HashSet的引用。您nthLoop的签名已经正确无误;您只需删除一些&

要解除分配s2,您可以撰写drop(s2)。请注意,Rust没有保证尾部调用,因此每次递归调用仍然会占用一些堆栈空间(您可以看到mem::size_of函数的多少),但drop调用将会清除堆上的数据。

  

来电者看起来像这样:

同样,您只需要在此处删除&

  

请注意,我还没有打扰到iterNeighbors的电话。

           

如果我手动内联闭包,但是我无法弄清楚如何调用闭包,这是有效的。理想情况下,我喜欢静态发送。

Rust中有三种类型的闭包:FnFnMutFnOnce。它们的self参数的类型不同。区别很重要,因为它限制了允许关闭的内容以及调用者如何使用闭包。 Rust书籍a chapter on closures已经很好地解释了这一点。

你的关闭需要改变s0。但是,iterNeighbors被定义为期望Fn关闭。您的关闭无法实现Fn因为Fn收到&self,但要改变s0,您需要&mut selfiterNeighbors无法使用FnOnce,因为它需要多次调用闭包。因此,您需要使用FnMut

此外,没有必要通过引用iterNeighbors来传递闭包。你可以按值传递它;每次对闭包的调用只会借用闭包,而不是消耗它。

  

另外,我只在F#中使用了HashSet,因为我认为Rust没有提供具有有效集合理论操作(并集,交集和差异)的纯函数集。我是否正确地假设?

标准库中没有纯粹的功能集实现(可能crates.io上有一个?)。虽然Rust包含函数式编程,但它还利用其所有权和借用系统来使命令式编程更安全。函数集可能会使用某种形式的引用计数或垃圾收集来强制跨集合共享项目。

但是,HashSet确实实现了集合理论操作。有两种方法可以使用它们:迭代器(differencesymmetric_differenceintersectionunion),它们生成延迟的序列,或运算符(|&中列出的^-use std::collections::HashSet; fn iterNeighbors<F>(mut f: F, (i, j): (i32, i32)) -> () where F: FnMut((i32, i32)) -> (), { f((i - 1, j)); f((i + 1, j)); f((i, j - 1)); f((i, j + 1)); } fn nthLoop(n: i32, s1: HashSet<(i32, i32)>, s2: HashSet<(i32, i32)>) -> HashSet<(i32, i32)> { if n == 0 { return s1; } else { let mut s0 = HashSet::new(); for &p in &s1 { let add = |p| { if !(s1.contains(&p) || s2.contains(&p)) { s0.insert(p); } }; iterNeighbors(add, p); } drop(s2); return nthLoop(n - 1, s0, s1); } } fn nth(n: i32, p: (i32, i32)) -> HashSet<(i32, i32)> { let mut s1 = HashSet::new(); s1.insert(p); let s2 = HashSet::new(); return nthLoop(n, s1, s2); } fn main() { let s = nth(2000, (0, 0)); println!("{}", s.len()); } ,它们会生成包含源集值的克隆的新集合。

这是工作代码:

length(c(1, 2, NULL, NULL, NULL))