web-dev-qa-db-ja.com

Rustで再帰的なクロージャーを作成することは可能ですか?

これは非常に単純な例ですが、次のようなことをするにはどうすればよいですか。

let fact = |x: u32| {
    match x {
        0 => 1,
        _ => x * fact(x - 1),
    }
};

この特定の例は反復で簡単に実行できることは知っていますが、Rustで、より複雑なもの(ツリーのトラバースなど)に対して再帰関数を作成できるかどうか疑問に思っています。代わりに自分のスタックを使用する必要があります。

39
Undeterminant

これを行うにはいくつかの方法があります。

クロージャーを構造体に入れ、この構造体をクロージャーに渡すことができます。関数内で構造体をインラインで定義することもできます。

fn main() {
    struct Fact<'s> { f: &'s Fn(&Fact, u32) -> u32 }
    let fact = Fact {
        f: &|fact, x| if x == 0 {1} else {x * (fact.f)(fact, x - 1)}
    };

    println!("{}", (fact.f)(&fact, 5));
}

これは、無限型(それ自体を引数として受け取る関数)の問題と、factlet fact = |x| {...}を書き込んだときにクロージャー自体の内部でまだ定義されていないという問題を回避します。そこでは参照できません。

これはRust 1.17で機能しますが、 ブログ投稿で説明されているように、場合によっては危険であるため、将来は違法になる可能性があります)繰り返し閉鎖の場合 。ただし、突然変異がないため、ここでは完全に安全です。


もう1つのオプションは、再帰関数をfnアイテムとして記述することです。これは、関数内でインラインで定義することもできます。

fn main() {
    fn fact(x: u32) -> u32 { if x == 0 {1} else {x * fact(x - 1)} }

    println!("{}", fact(5));
}

これは、環境から何もキャプチャする必要がない場合に正常に機能します。


もう1つのオプションは、fn itemソリューションを使用することですが、必要な引数/環境を明示的に渡します。

fn main() {
    struct FactEnv { base_case: u32 }
    fn fact(env: &FactEnv, x: u32) -> u32 {
        if x == 0 {env.base_case} else {x * fact(env, x - 1)}
    }

    let env =  FactEnv { base_case: 1 };
    println!("{}", fact(&env, 5));
}

これらはすべてRust 1.17で機能し、バージョン0.6以降で機能している可能性があります。fns内で定義されているfnは、トップレベルで定義されているものと同じです。 fn内でのみアクセス可能であることを除いて、それらは内部で定義されています。

31
huon

これが私が思いついた本当に醜く冗長な解決策です:

use std::{
    cell::RefCell,
    rc::{Rc, Weak},
};

fn main() {
    let weak_holder: Rc<RefCell<Weak<dyn Fn(u32) -> u32>>> =
        Rc::new(RefCell::new(Weak::<fn(u32) -> u32>::new()));
    let weak_holder2 = weak_holder.clone();
    let fact: Rc<dyn Fn(u32) -> u32> = Rc::new(move |x| {
        let fact = weak_holder2.borrow().upgrade().unwrap();
        if x == 0 {
            1
        } else {
            x * fact(x - 1)
        }
    });
    weak_holder.replace(Rc::downgrade(&fact));

    println!("{}", fact(5)); // prints "120"
    println!("{}", fact(6)); // prints "720"
}

これの利点は、期待されるシグネチャ(追加の引数は不要)で関数を呼び出すことです。これは、変数を(移動によって)キャプチャできるクロージャーであり、新しい構造体を定義する必要がなく、クロージャーは関数またはそれが作成されたスコープよりも長生きする場所に格納される(Rc<Fn...>)そしてそれはまだ動作します。

1
newacct