【问题标题】:How to apply trait inside a closure which is used as a function result如何在用作函数结果的闭包内应用特征
【发布时间】:2015-12-03 10:18:28
【问题描述】:

我无法实现这个 LISP 构造

(defun foo (n)
  (lambda (i) (incf n i)))

在 Rust 中。 我试过this:

use std::ops::Add;

fn f<T: Add>(n: T) -> Box<Fn(T) -> T> {
    Box::new(move |i: T| n + i)
} 

fn main() {
    let adder = f(2);
    assert_eq!(4, adder(2));
}

但它会导致错误:

error: mismatched types:
 expected `T`,
    found `<T as core::ops::Add>::Output`
(expected type parameter,
    found associated type) [E0308]
           Box::new(move |i: T| n + i)
                                ^~~~~

似乎为外部函数定义的特征Add 没有转移到内部闭包中。

这样的构造可以实现吗?

可以使用具体类型而不是泛型来实现此功能:

fn f(n: i32) -> Box<Fn(i32) -> i32> {
    Box::new(move |i| n + i)
} 

【问题讨论】:

  • 欢迎来到泛型 w.r.t 的痛苦。 Rust 中的算术运算!

标签: rust


【解决方案1】:

通用版本存在几个问题。

首先,您提供的错误是因为仅T: Add 不足以指定输出类型:您还需要对关联类型&lt;T as Add&gt;::Output 施加约束(请参阅Add 文档):

fn f<T: Add<Output=T>>(n: T) -> Box<Fn(T) -> T> {
    Box::new(move |i: T| n + i)
}

或者,你可以让闭包返回&lt;T as Add&gt;的输出类型:

fn f<T: Add>(n: T) -> Box<Fn(T) -> T::Output> {
    Box::new(move |i: T| n + i)
}

但是,现在您将收到以下错误:

<anon>:4:10: 4:37 error: the parameter type `T` may not live long enough [E0310]
<anon>:4     Box::new(move |i: T| n + i)
             ^~~~~~~~~~~~~~~~~~~~~~~~~~~
<anon>:4:10: 4:37 help: see the detailed explanation for E0310
<anon>:4:10: 4:37 help: consider adding an explicit lifetime bound `T: 'static`...
<anon>:4:10: 4:37 note: ...so that the type `[closure@<anon>:4:19: 4:36 n:T]` will meet its required lifetime bounds
<anon>:4     Box::new(move |i: T| n + i)
             ^~~~~~~~~~~~~~~~~~~~~~~~~~~

这里的问题是T 内部可能包含引用(之后,它是一个泛型类型 - 它可以包含任何东西);然而,Box&lt;Fn(T) -&gt; T&gt; 隐含地意味着这个特征对象内的任何东西都必须是'static,即编译器自动添加'static 约束:Box&lt;Fn(T) -&gt; T + 'static&gt;。但是,您的闭包会捕获 T,它可以包含任何引用,而不仅仅是 'static

修复它的最通用方法是添加TBox&lt;Fn(T) -&gt; T&gt; 的显式生命周期约束:

fn f<'a, T: Add<Output=T> + 'a>(n: T) -> Box<Fn(T) -> T + 'a> {
    Box::new(move |i: T| n + i)
}

或者,您可以指定 T'static,但这会不必要地限制代码的通用性:

fn f<T: Add<Output=T> + 'static>(n: T) -> Box<Fn(T) -> T> {
    Box::new(move |i: T| n + i)
}

但是,这仍然无法编译:

<anon>:4:31: 4:32 error: cannot move out of captured outer variable in an `Fn` closure
<anon>:4     Box::new(move |i: T| n + i)
                                  ^

这个错误的发生是因为 Rust 中的加法(即Add trait)按值起作用——它消耗了两个参数。对于Copy 类型,比如数字,这很好——它们总是被复制的。但是,编译器不能假设泛型类型参数也指定Copy 类型,因为没有相应的界限,因此它假设T 类型的值只能移动。但是,您指定返回的闭包是 Fn,因此它通过引用获取其环境。您无法移出引用,这就是此错误的原因。

有几种方法可以修复这个错误,最简单的一种是添加Copy bound:

fn f<'a, T: Add<Output=T> + Copy + 'a>(n: T) -> Box<Fn(T) -> T + 'a> {
    Box::new(move |i: T| n + i)
}

现在它可以编译了。

一种可能的替代方法是返回 FnOnce 闭包,它通过值获取其环境:

fn f<'a, T: Add<Output=T> + 'a>(n: T) -> Box<FnOnce(T) -> T + 'a> {
    Box::new(move |i: T| n + i)
}

但是,它有两个问题。首先,顾名思义,FnOnce 只能被调用一次,因为在它第一次调用时它的环境就被消耗掉了,下次就没有什么可以调用它了。这可能过于局限。其次,不幸的是,Rust 根本无法调用 Box&lt;FnOnce()&gt; 闭包。这是一个实现问题,应该在未来解决;目前有一个不稳定的FnBox trait 可以解决这个问题。

甚至另一种选择是使用引用而不是值:

fn f<'a, T: 'a>(n: T) -> Box<Fn(T) -> T + 'a> where for<'b> &'b T: Add<T, Output=T> {
    Box::new(move |i: T| &n + i)
} 

现在我们指定&amp;'b T 在任何生命周期内'b 必须与T 相加,而不是T。在这里,我们使用Add trait 被重载以引用原始类型的事实。这可能是这个函数最通用的版本。

【讨论】:

  • fn f&lt;'a, T: Add&lt;Output=T&gt; + Copy + 'a&gt;(n: T) -&gt; Box&lt;Fn(T) -&gt; T + 'a&gt; 工作正常,例如:assert_eq!(6, ff(2));,但 fn f2&lt;'a, T: Add&lt;Output=T&gt; + 'a&gt;(n: T) -&gt; Box&lt;FnOnce(T) -&gt; T + 'a&gt; 对我来说似乎没用。我应该如何使用 f2 ?
  • @seb_odessa 请阅读该示例下方的说明。我说有问题,包括调用Box&lt;FnOnce&gt;的问题。
【解决方案2】:

如果您可以摆脱函数调用,您可以将 x 包装成您自己的类型:

use std::ops::Add;

struct Adder<X> { 
    x: X 
}

impl<X: Copy> Adder<X> {
    fn add<Y: Add<X>>(&self, y: Y) -> <Y as Add<X>>::Output {
        y + self.x
    }
}

fn main() {
    let x = Adder { x: 1usize };

    x.add(2); // as opposed to x(2)
}

这意味着您可以摆脱Box 并且不需要分配任何东西。在稳定的 Rust 中实现 Fn(..) 是不可能的,并且不稳定的版本可能会在未来的 Rust 版本中中断。更多信息请查看std::ops::Fn

【讨论】:

  • 谢谢,但是实现一个返回没有辅助结构的函数的 fn 很有趣。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-17
  • 1970-01-01
  • 2019-09-14
  • 1970-01-01
  • 2022-12-11
相关资源
最近更新 更多