我正在尝试从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
。我是否正确地假设?
答案 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不提供具有有效集合理论操作(并集,交集和差异)的纯函数集。我是否正确地假设?
根据您的具体要求,不,例如, HashSet
和BTreeSet
都提供了methods which return iterators的各种集合论操作。
一些小问题:
s0.clear()
,但是,通过传递{递归版本可以实现相同的好处{1}}重新使用而不是丢弃它。s2
循环可以是while
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中有三种类型的闭包:Fn
,FnMut
和FnOnce
。它们的self
参数的类型不同。区别很重要,因为它限制了允许关闭的内容以及调用者如何使用闭包。 Rust书籍a chapter on closures已经很好地解释了这一点。
你的关闭需要改变s0
。但是,iterNeighbors
被定义为期望Fn
关闭。您的关闭无法实现Fn
因为Fn
收到&self
,但要改变s0
,您需要&mut self
。 iterNeighbors
无法使用FnOnce
,因为它需要多次调用闭包。因此,您需要使用FnMut
。
此外,没有必要通过引用iterNeighbors
来传递闭包。你可以按值传递它;每次对闭包的调用只会借用闭包,而不是消耗它。
另外,我只在F#中使用了HashSet,因为我认为Rust没有提供具有有效集合理论操作(并集,交集和差异)的纯函数集。我是否正确地假设?
标准库中没有纯粹的功能集实现(可能crates.io上有一个?)。虽然Rust包含函数式编程,但它还利用其所有权和借用系统来使命令式编程更安全。函数集可能会使用某种形式的引用计数或垃圾收集来强制跨集合共享项目。
但是,HashSet
确实实现了集合理论操作。有两种方法可以使用它们:迭代器(difference
,symmetric_difference
,intersection
,union
),它们生成延迟的序列,或运算符(|
, &
中列出的^
,-
,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))