在Rust中编写固定点函数

13

我刚刚开始学习Rust教程,最后用递归结束了以下代码

extern crate rand;

use std::io;
use rand::Rng;
use std::cmp::Ordering;
use std::str::FromStr;
use std::fmt::{Display, Debug};

fn try_guess<T: Ord>(guess: T, actual: T) -> bool {
    match guess.cmp(&actual) {
        Ordering::Less => {
            println!("Too small");
            false
        }
        Ordering::Greater => {
            println!("Too big");
            false
        }
        Ordering::Equal => {
            println!("You win!");
            true
        }
    }
}

fn guess_loop<T: Ord + FromStr + Display + Copy>(actual: T)
    where <T as FromStr>::Err: Debug
{
    println!("PLease input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess_int: T = guess.trim()
        .parse()
        .expect("Should enter integer number");

    println!("You guessed {} !", guess_int);

    if !try_guess(guess_int, actual) {
        guess_loop(actual)
    }
}

fn main() {
    println!("Guess the number!!!");

    let secret_number = rand::thread_rng().gen_range(1, 51);

    guess_loop(secret_number);

}

我希望将guess_loop函数中的递归分离出来,并引入不动点运算符进行修改:
fn guess_loop<T: Ord + FromStr + Display + Copy>(actual: T, recur: fn(T) -> ()) -> ()
    where <T as FromStr>::Err: Debug
{
    println!("PLease input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess_int: T = guess.trim()
        .parse()
        .expect("Should enter integer number");

    println!("You guessed {} !", guess_int);

    if !try_guess(guess_int, actual) {
        recur(actual)
    }
}

fn fix<T, R>(func: fn(T, fn(T) -> R) -> R) -> fn(T) -> R {
    fn fixed(val: T) -> R {
        func(val, fixed)
    }
    fixed
}

fn main() {
    println!("Guess the number!!!");

    let secret_number = rand::thread_rng().gen_range(1, 51);

    fix(guess_loop)(secret_number);
}

但这导致了许多错误,例如
error[E0401]: can't use type parameters from outer function; try using a local type parameter instead
  --> src/main.rs:49:19
   |
49 |     fn fixed(val: T) -> R {
   |                   ^ use of type variable from outer function

error[E0401]: can't use type parameters from outer function; try using a local type parameter instead
  --> src/main.rs:49:25
   |
49 |     fn fixed(val: T) -> R {
   |                         ^ use of type variable from outer function

error[E0434]: can't capture dynamic environment in a fn item; use the || { ... } closure form instead
  --> src/main.rs:50:9
   |
50 |         func(val, fixed)
   |         ^^^^

我的下一步尝试是更改guess_loop的定义为:
fn guess_loop<T: Ord + FromStr + Display + Copy, F>(actual: T, recur: F) -> ()
where <T as FromStr>::Err: Debug,
      F: Fn(T) -> ()
{ ... }

重新定义fix

fn fix<T, R, F>(func: fn(T, F) -> R) -> F
    where F: Fn(T) -> R
{
    let fixed = |val: T| func(val, fix(func));
    fixed
}

这导致了
error[E0308]: mismatched types
  --> src/main.rs:53:5
   |
53 |     fixed
   |     ^^^^^ expected type parameter, found closure
   |
   = note: expected type `F`
   = note:    found type `[closure@src/main.rs:52:17: 52:46 func:_]`

error: the type of this value must be known in this context
  --> src/main.rs:61:5
   |
61 |     fix(guess_loop)(secret_number);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

我该如何编写类似的fix函数?

Rust没有尾递归优化,因此不建议使用无限递归。最好使用迭代。 - Shepmaster
@Shepmaster 是的,这就是为什么我希望稍后将 fix 更像 Trampoline - Odomontois
4个回答

10
首先,变量名在初始化之前不存在。像这样让fixed引用自己是不行的。
其次,你不能从函数中按值返回闭包。泛型参数由调用者选择,而调用者不知道函数内部闭包的类型会是什么。
我不是在声称以下内容是做这件事情的最佳方法,但这是我能想出的最简单的经过类型检查的方式。
fn guess_loop<T>(actual: T, recur: &Fn(T)) -> ()
    where T: Ord + FromStr + Display + Copy,
          <T as FromStr>::Err: Debug
{
    // ...
}

fn fix<T, R, F>(func: F) -> Box<Fn(T) -> R>
    where T: 'static,
          R: 'static,
          F: Fn(T, &Fn(T) -> R) -> R + 'static
{
    use std::cell::RefCell;
    use std::rc::Rc;

    let fixed = Rc::new(RefCell::new(None));
    let fixed_fn = {
        let fixed = fixed.clone();
        move |val: T| -> R {
            let fixed_ref = fixed.borrow();
            let fixed_ref: &Box<_> = fixed_ref.as_ref().unwrap();
            func(val, &**fixed_ref)
        }
    };
    *fixed.borrow_mut() = Some(Box::new(fixed_fn));

    Box::new(move |val: T| -> R {
        let fixed_ref = fixed.borrow();
        let fixed_ref: &Box<_> = fixed_ref.as_ref().unwrap();
        fixed_ref(val)
    })
}

为了让fixed_fn引用自身,我们必须在它存在之前创建一个东西供它读取。不幸的是,这意味着有一个循环,而Rust 非常讨厌循环。因此,我们通过构建一个引用计数的RefCell<Option<_>>来解决这个问题,它从None开始,并将在稍后被改变以包含固定点闭包。
其次,我们不能将此句柄用作可调用函数,因此我们必须明确地提取闭包指针,以便我们可以将其传递给func
第三,编译器似乎无法正确推断fixed的类型。我希望它能够推断出它是Rc<RefCell<Option<{closure}>>>,但它拒绝这样做。因此,我们不得不采用存储Box<Fn(T) -> R>的方式,因为我们无法显式命名闭包的类型。
最后,我们必须构造一个新的闭包,它接受一个对fixed的第二个句柄,解包它并调用它。同样,我们不能直接使用fixed作为可调用函数。我们也不能在fixed内部重复使用闭包,因为这样做我们就必须将其放入自己的Rc中,在那一点上,事情开始变得疯狂。
最后,我们必须以Box的形式返回这个第二个闭包,因为正如我之前所说,我们无法通过值返回闭包,因为我们无法在签名中命名它们的类型。
如果有人有更简单的解决方案,我会很高兴看到它。:P

你认为后一种方法怎么样?是否可以使用一些带有 trait Fix 的存在定义 F 来完成? - Odomontois
2
@Odomontois 你不能按值返回一个闭包。我的意思是在“你不能吃太阳”的意义上,而不是“你不能吃整匹马(除非你真的很饿)”的意义上。任何聪明的尝试都无法避免这样做不了。一旦impl Trait稳定下来,你就可以这样做,但现在它还没有稳定,所以这并没有帮助。 - DK.
我不确定现在的 impl Trait 是否能够处理它。 - Shepmaster

5
这是对我自己提出的有关实现Y组合子的问题的回答,该问题是这个问题的一个子集。在纯lambda表达式中,Y组合子的一个版本看起来像:
λf.(λw.w w)(λw.f (w w))

Rosetta Code中的解决方案过于复杂并使用Box在堆上分配内存。我想简化它。

首先,让我们将类型Mu<T>实现为trait。

trait Mu<T> {
    fn unroll(&self, &Mu<T>) -> T;
}

请注意,我们需要这个trait是对象安全的,这意味着我们不能在其定义中要求Self,因此第二个参数被类型化为&Mu<T>并且它是一个trait对象。
现在我们可以编写一个通用的trait实现:
impl<T, F: Fn(&Mu<T>) -> T> Mu<T> for F {
    fn unroll(&self, o: &Mu<T>) -> T {
        self(o)
    }
}

有了这个,我们现在可以把Y组合子写成以下形式:
fn y<T, F: Fn(T) -> T>(f: &F) -> T {
    (&|w: &Mu<T>| w.unroll(w))(&|w: &Mu<T>| f(w.unroll(w)))
}

上述代码在 Rust playground中编译通过,没有启用任何功能,只使用稳定通道,因此这是对我的问题的很好回答。
然而,在实践中,上述代码将无法工作,因为Rust是按值调用的,但上述代码是按名字调用的Y组合器。
按值调用解决方案
要在不需要任何功能的情况下使用稳定通道,我们不能返回闭包(这需要impl Trait)。相反,我想出了另一个带有两个类型参数的Mu2类型。
trait Mu2<T, R> {
    fn unroll(&self, &Mu2<T, R>, t: T) -> R;
}

如上所述,让我们实现这个新特性。
impl<T, R, F> Mu2<T, R> for F
where
    F: Fn(&Mu2<T, R>, T) -> R,
{
    fn unroll(&self, o: &Mu2<T, R>, t: T) -> R {
        self(o, t)
    }
}

新的 Y 组合器:

fn y<T, R, F>(f: &F, t: T) -> R
where
    F: Fn(&Fn(T) -> R, T) -> R,
{
    (&|w: &Mu2<T, R>, t| w.unroll(w, t))((&|w: &Mu2<T, R>, t| f(&|t| w.unroll(w, t), t)), t)
}

现在是测试我们新设施的时候了。
fn main() {
    let fac = &|f: &Fn(i32) -> i32, i| if i > 0 { i * f(i - 1) } else { 1 };
    println!("{}", y(fac, 10))
}

结果为:

3628800

全部完成!

你可以看到,y函数的签名与提问者的fix略有不同,但这并不重要。

直接循环版本

为了避免返回闭包,可以使用相同的技术来处理普通的直接循环版本:

fn fix<T, R, F>(f: &F, t: T) -> R
where
    F: Fn(&Fn(T) -> R, T) -> R,
{
    f(&|t| fix(f, t), t)        
}

fn fib(i: i32) -> i32 {
    let fn_ = &|f:&Fn(i32) -> i32, x| if x < 2 { x } else { f(x-1) + f(x-2) };
    fix(fn_, i)
}

基本上,每当您需要从函数返回闭包时,可以将闭包的参数添加到函数中,并将返回类型更改为闭包的返回类型。稍后,当您需要一个真正的闭包时,只需通过部分求值该函数来创建闭包。 进一步讨论 与其他语言相比,在Rust中有一个很大的区别:给定用于查找固定点的函数必须没有任何内部状态。在Rust中,这是y的F类型参数必须是Fn而不是FnMut或FnOnce的要求。
例如,我们不能实现一个类似于fix_mut的函数。
fn fib1(i: u32) -> u32 {
    let mut i0 = 1;
    let mut i1 = 1;
    let fn_ = &mut |f:&Fn(u32) -> u32, x| 
        match x {
            0 => i0,
            1 => i1,
            _ => {
                let i2 = i0;
                i0 = i1;
                i1 = i1 + i2;
                f(x)
            }
        };

    fix_mut(fn_, i)
}

在这个版本中,即使不使用不安全代码,如果它能够正常工作,其性能(O(N))也比上面给出的版本(O(2^N))要好得多。
这是因为您一次只能拥有一个对象的一个可变引用。但是,Y组合器的想法,甚至是固定点函数,在调用它时需要同时捕获/传递函数,这是两个引用,您不能仅将其中任何一个标记为不可变而不标记另一个为不可变。
另一方面,我想知道我们是否可以做一些其他语言通常无法做到但Rust似乎能够做到的事情。我在考虑将F的第一个参数类型从Fn限制为FnOnce(因为y函数将提供实现,将其更改为FnMut没有意义,我们知道它不会有状态,但将其更改为FnOnce意味着我们只想使用它一次),Rust目前不允许,因为我们无法通过值传递未定大小的对象。
因此,基本上,这个实现是我们能想到的最灵活的解决方案。
顺便说一下,不可变限制的解决方法是使用伪变异:
fn fib(i: u32) -> u32 {
    let fn_ = &|f:&Fn((u32,u32,u32)) -> u32, (x,i,j)| 
        match x {
            0 => i,
            1 => j,
            _ => {
                f((x-1,j,i+j))
            }
        };
    fix(&fn_, (i,1,1))
}

4

从上次离开的地方开始:

fn fix<T, R, F>(func: fn(T, F) -> R) -> F
    where F: Fn(T) -> R
{
    |val: T| func(val, fix(func))
}

返回的对象具有不可命名的闭包类型。在这里使用泛型类型无济于事,因为闭包的类型是由被调用者决定的,而不是由调用者决定的。这就是impl特质变得非常方便的地方:
fn fix<T, R, F>(func: fn(T, F) -> R) -> impl Fn(T) -> R
    where F: Fn(T) -> R
{
    |val: T| func(val, fix(func))
}

我们无法将 fix(func) 传递给 func ,因为它期望一个可命名的类型用于 F。因此,我们只能使用trait对象来代替:

fn fix<T, R>(func: fn(T, &Fn(T) -> R) -> R) -> impl Fn(T) -> R {
    |val: T| func(val, &fix(func))
}

现在是时候对抗生命周期检查器了。编译器会报错:
only named lifetimes are allowed in `impl Trait`, but `` was found in the type `…`

这是一条有点神秘的消息。由于impl traits默认始终为“static”,这是一种迂回的方式来表达:闭包的寿命不足以满足“静态”的要求。为了获得真正的错误信息,我们需要将“+ 'static”附加到“impl Fn(T) -> R”并重新编译:

closure may outlive the current function, but it borrows `func`, which is owned by the current function

所以这就是真正的问题。它正在借用func。我们不需要借用func,因为fnCopy,所以我们可以随意复制它。让我们在闭包前面加上move并且去掉之前的+ 'static

fn fix<T, R>(func: fn(T, &Fn(T) -> R) -> R) -> impl Fn(T) -> R {
    move |val: T| func(val, &fix(func))
}

大功告成,它奏效了!嗯,几乎……你需要编辑guess_loop,将fn(T) -> ()更改为&Fn(T) -> ()。我真的很惊讶这个解决方案不需要任何分配。

如果您无法使用impl特性,则可以编写以下内容:

fn fix<T, R>(func: fn(T, &Fn(T) -> R) -> R) -> Box<Fn(T) -> R>
    where T: 'static,
          R: 'static
{
    Box::new(move |val: T| func(val, fix(func).as_ref()))
}

很遗憾,这并不是无需分配内存的。

此外,我们可以将结果推广一下,允许任意闭包和生命周期:

fn fix<'a, T, R, F>(func: F) -> impl 'a + Fn(T) -> R
    where F: 'a + Fn(T, &Fn(T) -> R) -> R + Copy
{
    move |val: T| func(val, &fix(func))
}

在解决你的问题过程中,我写了一个更为简单的版本的“fix”,这实际上引导我找到了解决“你的”“fix”函数的方法。
type Lazy<'a, T> = Box<FnBox() -> T + 'a>;

// fix: (Lazy<T> -> T) -> T
fn fix<'a, T, F>(f: F) -> T
    where F: Fn(Lazy<'a, T>) -> T + Copy + 'a
{
    f(Box::new(move || fix(f)))
}

这里是如何使用this fix函数计算阶乘的演示:
fn factorial(n: u64) -> u64 {
    // f: Lazy<u64 -> u64> -> u64 -> u64
    fn f(fac: Lazy<'static, Box<FnBox(u64) -> u64>>) -> Box<FnBox(u64) -> u64> {
        Box::new(move |n| {
            if n == 0 {
                1
            } else { 
                n * fac()(n - 1)
            }
        })
    }
    fix(f)(n)
}

@Shepmaster 我不知道Rust风格要求{放在下一行。 - Rufflewind
1
我的流程是将代码复制,粘贴到 playground 中,并运行 rustfmt。这使用默认设置,也就是我所理解的事实上的通用风格。具体来说,当有 where 子句时,{ 将放在下一行,每个 where 子句都放在独立的一行上。但并非所有情况下 { 都会放在下一行。 - Shepmaster
谢谢提供信息! - Rufflewind

2

如果您愿意使用不稳定的功能(即夜间编译器)并且愿意略微混淆代码,那么可以在零运行时成本下完成此操作。

首先,我们需要将fix的结果转换为命名结构。该结构需要实现Fn,因此我们将手动实现它(这是一个不稳定的功能)。

    #![feature(fn_traits)]
    #![feature(unboxed_closures)]

extern crate rand;

use rand::Rng;
use std::cmp::Ordering;

fn try_guess<T: Ord>(guess: T, actual: T) -> bool {
    match guess.cmp(&actual) {
        Ordering::Less => {
            println!("Too small");
            false
        }
        Ordering::Greater => {
            println!("Too big");
            false
        }
        Ordering::Equal => {
            println!("You win!");
            true
        }
    }
}

struct Fix<F>
    where F: Fn(i32, &Fix<F>)
{
    func: F,
}

impl<F> FnOnce<(i32,)> for Fix<F>
    where F: Fn(i32, &Fix<F>)
{
    type Output = ();

    extern "rust-call" fn call_once(self, args: (i32,)) -> Self::Output {
        self.call(args)
    }
}

impl<F> FnMut<(i32,)> for Fix<F>
    where F: Fn(i32, &Fix<F>)
{
    extern "rust-call" fn call_mut(&mut self, args: (i32,)) -> Self::Output {
        self.call(args)
    }
}

impl<F> Fn<(i32,)> for Fix<F>
    where F: Fn(i32, &Fix<F>)
{
    extern "rust-call" fn call(&self, (val,): (i32,)) -> Self::Output {
        (self.func)(val, self);
    }
}

fn fix<F>(func: F) -> Fix<F>
    where F: Fn(i32, &Fix<F>)
{
    Fix { func: func }
}

fn guess_loop<F>(actual: i32, recur: &F)
    where F: Fn(i32)
{
    let guess_int = rand::thread_rng().gen_range(1, 51);

    if guess_int != actual {
        recur(actual)
    }
}

fn main() {
    let secret_number = rand::thread_rng().gen_range(1, 51);

    fix(guess_loop)(secret_number);
}

然而,我们还没有完成。这会导致以下错误编译失败:

error[E0281]: type mismatch: the type `fn(i32, &_) {guess_loop::<_>}` implements the trait `for<'r> std::ops::Fn<(i32, &'r _)>`, but the trait `for<'r> std::ops::Fn<(i32, &'r Fix<fn(i32, &_) {guess_loop::<_>}>)>` is required (cyclic type of infinite size)
  --> src/main.rs:77:5
   |
77 |     fix(guess_loop)(secret_number);
   |     ^^^
   |
   = note: required by `fix`

注意:如果您不知道,在 Rust 中每个函数都有自己的零大小类型。 如果一个函数是泛型的,那么该函数的每个实例也将拥有自己的类型。 例如,编译器将报告 `guess_loop::<X>` 的类型为 `fn(i32, &X) {guess_loop::<X>}`(如上面的错误消息所示,未解析出具体类型的位置使用下划线替代)。 在某些上下文中,该类型可以隐式地强制转换为函数指针类型,或者可以使用显式转换(`as`)进行强制类型转换。
问题在于,在表达式 `fix(guess_loop)` 中,编译器需要实例化 `guess_loop`,这是一个泛型函数,看起来编译器无法找出正确的类型来实例化它。 实际上,我们想要为类型参数 `F` 设置的类型引用了 `guess_loop` 的类型。 如果按照编译器报告的样式写出来,类型看起来像 `fn(i32, &Fix<X>) {guess_loop::<Fix<&X>>}`,其中 `X` 被类型本身所替换(现在您可以看到“无限大小的循环类型”来自哪里)。
我们可以通过将 `guess_loop` 函数替换为一个实现对自身的引用的非泛型结构体(我们将其称为 `GuessLoop`),从而解决此问题。 (不能对普通函数这样做,因为无法命名函数的类型。)
struct GuessLoop;

impl<'a> FnOnce<(i32, &'a Fix<GuessLoop>)> for GuessLoop {
    type Output = ();

    extern "rust-call" fn call_once(self, args: (i32, &Fix<GuessLoop>)) -> Self::Output {
        self.call(args)
    }
}

impl<'a> FnMut<(i32, &'a Fix<GuessLoop>)> for GuessLoop {
    extern "rust-call" fn call_mut(&mut self, args: (i32, &Fix<GuessLoop>)) -> Self::Output {
        self.call(args)
    }
}

impl<'a> Fn<(i32, &'a Fix<GuessLoop>)> for GuessLoop {
    extern "rust-call" fn call(&self, (actual, recur): (i32, &Fix<GuessLoop>)) -> Self::Output {
        let guess_int = rand::thread_rng().gen_range(1, 51);

        if !try_guess(guess_int, actual) {
            recur(actual)
        }
    }
}

fn main() {
    let secret_number = rand::thread_rng().gen_range(1, 51);

    fix(GuessLoop)(secret_number);
}

需要注意的是,GuessLoop中的Fn实现不再针对recur参数类型进行通用。如果我们尝试使Fn的实现变成通用的(同时仍然保持结构体本身非通用,以避免循环类型),会发生什么?

struct GuessLoop;

impl<'a, F> FnOnce<(i32, &'a F)> for GuessLoop
    where F: Fn(i32),
{
    type Output = ();

    extern "rust-call" fn call_once(self, args: (i32, &'a F)) -> Self::Output {
        self.call(args)
    }
}

impl<'a, F> FnMut<(i32, &'a F)> for GuessLoop
    where F: Fn(i32),
{
    extern "rust-call" fn call_mut(&mut self, args: (i32, &'a F)) -> Self::Output {
        self.call(args)
    }
}

impl<'a, F> Fn<(i32, &'a F)> for GuessLoop
    where F: Fn(i32),
{
    extern "rust-call" fn call(&self, (actual, recur): (i32, &'a F)) -> Self::Output {
        let guess_int = rand::thread_rng().gen_range(1, 51);

        if !try_guess(guess_int, actual) {
            recur(actual)
        }
    }
}

很不幸,编译失败并出现以下错误:

error[E0275]: overflow evaluating the requirement `<Fix<GuessLoop> as std::ops::FnOnce<(i32,)>>::Output == ()`
  --> src/main.rs:99:5
   |
99 |     fix(GuessLoop)(secret_number);
   |     ^^^
   |
   = note: required because of the requirements on the impl of `for<'r> std::ops::Fn<(i32, &'r Fix<GuessLoop>)>` for `GuessLoop`
   = note: required by `fix`

简而言之,编译器无法验证Fix<GuessLoop>是否实现了Fn(i32),因为要做到这一点,它需要验证GuessLoop是否实现了Fn(i32, &Fix<GuessLoop>),但只有在Fix<GuessLoop>实现了Fn(i32)的情况下才成立(因为该impl是有条件的),而只有在GuessLoop实现了Fn(i32, &Fix<GuessLoop>)的情况下才成立(因为该impl也是有条件的),你可以想象一下。换句话说,这里的两个Fn实现是相互依存的,编译器无法解决这个问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接