Write fix point function in Rust

后端 未结 4 1465
醉梦人生
醉梦人生 2021-01-11 13:42

I\'ve just started Rust tutorial and ended with such code using recursion

extern crate rand;

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


        
4条回答
  •  执笔经年
    2021-01-11 14:04

    This is an answer to my own question about implementing the Y combinator which is a subset of this question. In pure lambda expression, a version of the Y combinator looks like

    λf.(λw.w w)(λw.f (w w))
    

    The solution in Rosetta Code is too complicated and used Box to allocate memory in the heap. I want to simplify this.

    First, let's implement the type Mu as a trait instead.

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

    Note that we need this trait to be object safe, which means we cannot ask for Self in any of its definition so the second parameter is typed &Mu and it is a trait object.

    Now we can write a generic trait implementation:

    impl) -> T> Mu for F {
        fn unroll(&self, o: &Mu) -> T {
            self(o)
        }
    }
    

    With this, we can now write the y combinator as the following:

    fn y T>(f: &F) -> T {
        (&|w: &Mu| w.unroll(w))(&|w: &Mu| f(w.unroll(w)))
    }
    

    The above compiles in the Rust playground without enabling any features and using only the stable channel so this is a pretty good answer to my question.

    However, the above would not work in practice because Rust is call-by-value but the code above is the call-by-name Y combinator.

    The call-by-value solution

    To work with the stable channel without requiring any features, we cannot return closures (which requires impl Trait). Instead, I came up with making another Mu2 type that takes two type parameters:

    trait Mu2 {
        fn unroll(&self, &Mu2, t: T) -> R;
    }
    

    As above, let's implement this new trait.

    impl Mu2 for F
    where
        F: Fn(&Mu2, T) -> R,
    {
        fn unroll(&self, o: &Mu2, t: T) -> R {
            self(o, t)
        }
    }
    

    The new Y combinator:

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

    Now it is time to test our new facility.

    fn main() {
        let fac = &|f: &Fn(i32) -> i32, i| if i > 0 { i * f(i - 1) } else { 1 };
        println!("{}", y(fac, 10))
    }
    

    Results in:

    3628800
    

    All done!

    You can see that the y function has a slightly different signature than the questioner's fix, but it shouldn't matter.

    The direct recurring version

    The same technology to avoid returning a closure can be used for the normal direct recurring version as well:

    fn fix(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)
    }
    

    Basically, whenever you need to return a closure from a function, you can add the closure's parameter to the function, and change the return type to the closure's return type. Later on when you need a real closure, just create the closure by partial evaluating that function.

    Further discussions

    Compare to other languages, in Rust there is a big difference: the function given to find fix point must not have any internal states. In Rust this is a requirement that the F type parameter of y must be Fn, not FnMut or FnOnce.

    For example, we cannot implement a fix_mut that would be used like

    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)
    }
    

    without unsafe code whilst this version, if it works, performs much better (O(N)) than the version given above (O(2^N)).

    This is because you can only have one &mut of one object at a single time. But the idea of Y combinator, or even the fix point function, requires capturing/passing the function at the same time when calling it, that's two references and you can't just mark any of them immutable without marking another so.

    On the other hand, I was wonder if we could do something that other languages usually not able to but Rust seems to be able. I was thinking restricting the first argument type of F from Fn to FnOnce (as y function will provide the implementation, change to FnMut does not make sense, we know it will not have states, but change to FnOnce means we want it to be used only once), Rust would not allow at the moment as we cannot pass unsized object by value.

    So basically, this implementation is the most flexible solution we could think of.

    By the way, the work around of the immutable restriction is to use pseudo-mutation:

    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))
    }
    

提交回复
热议问题