簡體   English   中英

在Rust中掙扎着關閉和生命

[英]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不提供具有有效集合理論操作(並集,交集和差異)的純函數集。 我認為這是正確的嗎?

根據您的需要,不會,例如, HashSetBTreeSet提供各種集合理論操作作為返回迭代器的方法


一些小點:

  • 顯式/命名生命周期允許編譯器推斷數據的靜態有效性,它們不控制它(即它們允許編譯器在你做錯的時候指出,但是語言仍然具有相同類型的靜態資源使用/生命循環保證為C ++)
  • 帶有循環的版本可能在編寫時更有效,因為它直接重用內存(交換集合,加上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中有三種類型的閉包: FnFnMutFnOnce 他們的self論證的類型不同。 區別很重要,因為它限制了允許關閉的內容以及調用者如何使用閉包。 Rust書中有一章關於閉包 ,已經很好地解釋了這一點。

你的閉包需要改變s0 但是, iterNeighbors定義為期望Fn閉包。 你的閉包不能實現Fn因為Fn接收&self ,但要改變s0 ,你需要&mut self iterNeighbors不能使用FnOnce ,因為它需要不止一次調用閉包。 因此,您需要使用FnMut

此外,沒有必要通過引用iterNeighbors來傳遞閉包。 你可以按值傳遞它; 每次對閉包的調用都只會借用閉包,而不是消耗它。

另外,我只在F#中使用了HashSet,因為我認為Rust不提供具有有效集合理論操作(並集,交集和差異)的純函數集。 我認為這是正確的嗎?

標准庫中沒有純粹的功能集實現(也許在crates.io上有一個?)。 雖然Rust包含函數式編程,但它還利用其所有權和借用系統來使命令式編程更安全。 函數集可能會使用某種形式的引用計數或垃圾回收來強制使用集合來共享項目。

但是, HashSet確實實現了集合理論操作。 有兩種方法可以使用它們:迭代器( differencesymmetric_differenceintersectionunion ),它們生成延遲的序列,或者運算符( |&^- ,如HashSettrait實現中所列),它們產生新的集合包含源集中值的克隆。


這是工作代碼:

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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM