[英]Struggling with closures and lifetimes in 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
的調用。
我想我正在努力讓參數的生命周期正確,因為它們在遞歸調用中被旋轉。 如果我希望在return
s之前釋放s2
並且我希望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
。 我認為這是正確的嗎?
我似乎必須加倍支架:這是正確的嗎?
否:雙括號是因為你選擇使用元組並調用一個帶元組的函數需要先創建元組,但是可以有一個帶有多個參數的閉包,比如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
閉包特征不允許這樣做。 可以以更靈活的方式(共享時)調用該特征,但是這會對封閉在內部執行的操作施加更多限制。 (如果你有興趣, 我已經寫了更多關於這個 )
幸運的是,有一個簡單的解決方法:使用FnMut
trait,這要求只有在對其具有唯一訪問權限時才能調用閉包,但允許內部變異。
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
提供各種集合理論操作作為返回迭代器的方法 。
一些小點:
s0.clear()
,但是,通過將s2
向下傳遞以便重用,可以通過遞歸版本實現相同的好處而不是放棄它。 while
循環可以是for _ in 0..n
foo(x, |y| bar(y + 1))
而不是foo(&|y| bar(y + 1), x)
) 尾隨返回不需要return
關鍵字(如果省略;
):
fn nth(n: i32, p: (i32, i32)) -> HashSet<(i32, i32)> { let s0 = HashSet::new(); let mut s1 = HashSet::new(); s1.insert(p); nthLoop(n, s1, s0) }
我認為這是一個接受閉包的函數(它本身接受一對並返回單位)和一對並返回單位。 我似乎必須加倍支架:這是正確的嗎?
你需要雙括號,因為你將2元組傳遞給閉包,它與原始的F#代碼相匹配。
我想我正在努力讓參數的生命周期正確,因為它們在遞歸調用中被旋轉。 如果我希望s2在返回之前被釋放並且我希望s1在返回時或者在遞歸調用中存活時,我應該如何注釋生命周期?
問題是當你應該直接使用HashSet
時,你正在使用對HashSet
的引用。 您對nthLoop
簽名已經是正確的; 你只需要刪除一些&
。
要解除分配s2
,可以寫drop(s2)
。 請注意,Rust沒有保證尾調用,因此每次遞歸調用仍然會占用一些堆棧空間(您可以看到mem::size_of
函數有多少),但drop
調用將清除堆上的數據。
調用者看起來像這樣:
同樣,你只需要刪除這里的&
。
請注意,我還沒有打擾到iterNeighbors的調用。
如果我手動內聯閉包,但是我無法弄清楚如何調用閉包,這是有效的。 理想情況下,我想在這里靜態發送。
Rust中有三種類型的閉包: Fn
, FnMut
和FnOnce
。 他們的self
論證的類型不同。 區別很重要,因為它限制了允許關閉的內容以及調用者如何使用閉包。 Rust書中有一章關於閉包 ,已經很好地解釋了這一點。
你的閉包需要改變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
),它們生成延遲的序列,或者運算符( |
, &
, ^
, -
,如HashSet
的trait實現中所列),它們產生新的集合包含源集中值的克隆。
這是工作代碼:
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());
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.