如何在Rust中组合函数?

90

我正在尝试编写一个组合两个函数的函数。最初的设计非常简单:一个接受两个函数并返回一个组合函数的函数,然后我可以将其与其他函数组合,因为Rust没有rest参数。但是,我遇到了令人沮丧的、没有帮助的编译器错误。

我的组合函数:

fn compose<'a, A, B, C, G, F>(f: F, g: G) -> Box<Fn(A) -> C + 'a>
where
    F: 'a + Fn(A) -> B + Sized,
    G: 'a + Fn(B) -> C + Sized,
{
    Box::new(move |x| g(f(x)))
}

我想要如何使用它:

fn main() {
    let addAndMultiply = compose(|x| x * 2, |x| x + 2);
    let divideAndSubtract = compose(|x| x / 2, |x| x - 2);

    let finally = compose(*addAndMultiply, *divideAndSubtract);
    println!("Result is {}", finally(10));
}

编译器不喜欢那样,无论我尝试什么,特性边界都无法满足。错误信息如下:
error[E0277]: the size for values of type `dyn std::ops::Fn(_) -> _` cannot be known at compilation time
  --> src/main.rs:13:19
   |
13 |     let finally = compose(*addAndMultiply, *divideAndSubtract);
   |                   ^^^^^^^ doesn't have a size known at compile-time
   |
   = help: the trait `std::marker::Sized` is not implemented for `dyn std::ops::Fn(_) -> _`
   = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
note: required by `compose`
  --> src/main.rs:1:1
   |
1  | / fn compose<'a, A, B, C, G, F>(f: F, g: G) -> Box<Fn(A) -> C + 'a>
2  | | where
3  | |     F: 'a + Fn(A) -> B + Sized,
4  | |     G: 'a + Fn(B) -> C + Sized,
5  | | {
6  | |     Box::new(move |x| g(f(x)))
7  | | }
   | |_^

2
关于主要目标,您可能正在寻找这个链接:https://stackoverflow.com/q/36284637/1233251 - E net4
不适用于我的情况。 - Web3 Philosopher
3个回答

142

正如@ljedrz指出的那样,要使其起作用,您只需要再次引用组合函数:

let finally = compose(&*multiply_and_add, &*divide_and_subtract);

(请注意,在Rust中,约定俗成的是变量名应该采用蛇形命名法)


不过我们可以做得更好!

自 Rust 1.26 起,我们可以使用抽象返回类型(之前被标记为#![feature(conservative_impl_trait)])。这可以帮助简化示例代码,因为它允许您跳过生命周期、引用、Sized 约束和 Box

fn compose<A, B, C, G, F>(f: F, g: G) -> impl Fn(A) -> C
where
    F: Fn(A) -> B,
    G: Fn(B) -> C,
{
    move |x| g(f(x))
}

fn main() {
    let multiply_and_add = compose(|x| x * 2, |x| x + 2);
    let divide_and_subtract = compose(|x| x / 2, |x| x - 2);

    let finally = compose(multiply_and_add, divide_and_subtract);
    println!("Result is {}", finally(10));
}

最后,既然你提到了rest parameters,我猜想你实际上想要的是一种能够以灵活的方式链式组合任意多个函数的方法。我为此编写了这个宏:

macro_rules! compose {
    ( $last:expr ) => { $last };
    ( $head:expr, $($tail:expr), +) => {
        compose_two($head, compose!($($tail),+))
    };
}

fn compose_two<A, B, C, G, F>(f: F, g: G) -> impl Fn(A) -> C
where
    F: Fn(A) -> B,
    G: Fn(B) -> C,
{
    move |x| g(f(x))
}

fn main() {
    let add = |x| x + 2;
    let multiply = |x| x * 2;
    let divide = |x| x / 2;
    let intermediate = compose!(add, multiply, divide);

    let subtract = |x| x - 2;
    let finally = compose!(intermediate, subtract);

    println!("Result is {}", finally(10));
}

7
根据个人口味,有些人可能想在参数位置使用“impl Trait”来进一步简化事情。 - gnzlbg
1
compose_two is not strictly necessary. Inlining the function inside the macro works but may produce terrible compile errors when the types don't match: ( $head:expr, $($tail:expr), +) => { |x| compose!($($tail),+)($head(x)) } - Victor Savu
1
听起来可能徒劳无功,但是熟悉编程的人会发现 add_and_multiply 实际上应该被称为 multiply_and_add,正如上面的实现所示。不过回答得很好。 - Orco
1
这适用于带有多个参数的函数吗?虽然我猜可以传递元组。Rust没有像Haskell那样,多个参数等效于这些参数的元组的好特性。 - Raskell

13

只需在finally块中添加引用即可使其正常工作:

fn main() {
    let addAndMultiply = compose(|x| x * 2, |x| x + 2);
    let divideAndSubtract = compose(|x| x / 2, |x| x - 2);

    let finally = compose(&*addAndMultiply, &*divideAndSubtract);
    println!("Result is {}", finally(10));
}

对于 addAndMultiply 或者 divideAndSubtract 进行解引用会暴露一个不满足 Sized 特性的 trait 对象;需要将其包装在一个 Box 中或者进行引用,以便将其传递给具有 Sized 约束的函数。


3

很好。我认为更加关注顺序的例子会更好,比如(x + 7) * 3,就像下面这个链接中的例子:https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=62ed96bf24062c6f17d0687046915913 - George

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