以一个超简单的struct foo为例:

#[derive(Debug)]
struct Foo {
    a: i32
}

还有一个作曲宏得了here分:

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

我可以定义一个简单的函数,该函数接受可变引用并修改 struct 并返回传递给它的引用:

fn foo(x: &mut Foo) -> &mut Foo {
    x.a = x.a * 2;
    x
}

它的工作方式与预期一致:

fn main() {
    let mut x = Foo { a: 3 };
    let y = foo(&mut x);
    println!("{:?}", y.a); // prints 6
    y.a = 7;
    println!("{:?}", x); // prints Foo { a: 7 }
}

当我try 定义第二个简单函数并组合这两个函数时,问题出现了:

fn bar(x: &mut Foo) -> &mut Foo {
    x.a = x.a + 1;
    x
}

fn main() {
    let baz = compose!(foo, bar);
    let mut x = Foo { a: 3 };
    let y = baz(&mut x);
    println!("{:?}", y.a);
}

我得到了一个错误,在main let y = baz(&mut x);中x的可变borrow 不够长.我不认为我理解得足够好,不能理解哪里出了问题.

另外,当我在第一个版本中打印绑定到x的 struct 时,它可以工作,因为它是在最后一次使用可变的borrow y之后,所以我可以不变地borrow x来打印它.但在第二个版本中,如果我try 在最后打印x,它会显示它仍然是静默借来的.组合宏中的某些东西似乎正在"抓住"x的可变借入?

我怎么才能让它起作用?让它起作用吗?

Playground

Edit based on comments:

看起来,虽然compose_two中的闭包实际上并没有保持对 struct 的可变引用,但返回类型并没有指定它没有(关闭close over捕获的变量,对吗?),因此编译器被迫假设它可能会.我如何让编译器相信我没有持有该引用?

推荐答案

Can个这样能行得通吗?

不是的.但根据您的用例,可能是这样的.

如果满足以下任一条件,则可以使其正常工作:

  • 您可以将函数类型限制为Copy(如果您使用的是函数项(fn),则它们始终为Copy,但对于闭包,如果它们捕获非Copy类型,则这可能是一个问题).
  • 你可以每晚使用.
  • 您可以更改compose!()(main())的用户.
  • 您可以将compose!()限制为引用(准确地说,是可变引用,但您也可以为共享引用创建一个版本.当然,如果您想为引用和拥有的类型创建单独的版本,这是很好的).

这里有三个因素,它们结合在一起使编译器x在其生命周期之后可以使用.如果我们打破其中一个,它会起作用的.其中两个实际上是假的,但是编译器不知道这一点(或者不想依赖它).这些因素包括:

  1. 编译器相信返回的闭包可以捕获它的参数.这是假的,我将立即解释,但编译器不知道这一点.
  2. 编译器认为闭包有一个Drop实现,并且可以在这个Drop中使用x(在步骤1中捕获).事实上,编译器知道它不知道,但因为我们使用了impl Trait,所以它被迫将其视为实现了Drop,所以添加一个不会是一个 destruct 性的更改.
  3. baz之前,x下降了.这是正确的(变量以与其声明相反的顺序被删除),再结合编译器之前的两个信念,这意味着当baz将(潜在地)在其删除中使用其捕获的x时,它将在x的生命周期之后.

让我们从最后一个索赔开始.这是最容易打破的,因为你只需要交换xbaz的顺序:

fn main() {
    let mut x = 3;
    let baz = compose_two(foo, bar);
    let y = baz(&mut x);
    println!("{:?}", y);
}

但并不总是可以更改main(),或者在baz之前声明x可能是不可能的.

因此,让我们回到第二个主张.编译器认为闭包有DropIml,因为它在impl Trait中.如果情况并非如此呢?

不幸的是,这需要每晚执行一次,因为手动编写闭包需要功能fn_traitsunboxed_closures.但这绝对是可能的(一个很好的附带好处是,根据其输入函数的不同,该函数可以有条件地为FnOnce/FnMut/Fn):

#![feature(fn_traits, unboxed_closures)]

struct ComposeTwo<G, F>(F, G);

impl<A, B, C, G, F> std::ops::FnOnce<(A,)> for ComposeTwo<G, F>
where
    F: FnOnce(A) -> B,
    G: FnOnce(B) -> C,
{
    type Output = C;

    extern "rust-call" fn call_once(self, (x,): (A,)) -> Self::Output {
        (self.1)((self.0)(x))
    }
}

impl<A, B, C, G, F> std::ops::FnMut<(A,)> for ComposeTwo<G, F>
where
    F: FnMut(A) -> B,
    G: FnMut(B) -> C,
{
    extern "rust-call" fn call_mut(&mut self, (x,): (A,)) -> Self::Output {
        (self.1)((self.0)(x))
    }
}

impl<A, B, C, G, F> std::ops::Fn<(A,)> for ComposeTwo<G, F>
where
    F: Fn(A) -> B,
    G: Fn(B) -> C,
{
    extern "rust-call" fn call(&self, (x,): (A,)) -> Self::Output {
        (self.1)((self.0)(x))
    }
}

fn compose_two<G, F>(f: F, g: G) -> ComposeTwo<G, F> {
    ComposeTwo(f, g)
}

打破这一假设的另一种方式是通过使返回的闭包Copy.Copy类型永远不能实现Drop,编译器知道这一点,并假定它们不实现.不幸的是,因为闭包捕获了fg,所以它们也需要是Copy:

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

最后一种方式是最难解释的.首先,我需要解释为什么编译器认为闭包可以捕获x,而实际上它不能.

让我们首先想一想为什么闭包不能做到这一点:它将在多长时间内取代下面的'?

struct Closure {
    f: some_function_type,
    g: some_function_type,
    captured_x: Option<&'? mut Foo>,
}

baz被定义时(我们必须决定我们将使用哪个生命周期),我们仍然不知道什么将被传递给闭包,所以我们不知道我们应该使用哪个生命周期!

这一知识本质上是"any年后才能调用闭包",在Rust中通过了Higher-Ranked Trait Bounds (HRTB),拼写为for<'lifetime>.所以,compose_two()人中的A人应该是HRTB.

但问题就在这里:泛型参数不能是HRTB.它们必须实例化为concrete个生命周期.因此,编译器为baz Select 了某个生存期'x,这个生存期必须大于baz本身-否则它将包含一个悬垂的生存期-因此理论上它可以有一个具有该生存期的成员,因此编译器认为baz可以存储对x的引用,而实际上它不能.

如果我们能成为HRTB就好了.

我们可以的!如果我们不将其完全泛型,而是将其指定为引用:

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

或者,使用省略形式,因为HRTB是Fn个性状界限的默认值:

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

不幸的是,它也需要B: 'static,因为编译器不能得出B是否足够长的结论(该语言的另一个限制),但它仍然有效!

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

Rust相关问答推荐

Rust中的相互递归特性与默认实现

有没有办法模仿对象安全克隆?

使用模块中的所有模块,但不包括特定模块

链表堆栈溢出

使用 select 处理 SIGINT 和子等待!无阻塞

结果流到 Vec 的结果:如何避免多个into_iter和collect?

一次不能多次borrow *obj作为可变对象

Rust 并行获取对 ndarray 的每个元素的可变引用

返回迭代器考虑静态生命周期类型

在 Rust 中查找向量中 dyn struct 的索引

为什么这个闭包没有实现Fn?

Rust中的位移操作对范围有什么影响?

在 Bevy 项目中为 TextureAtlas 精灵实施 NearestNeighbor 的正确方法是什么?

特征中定义的类型与一般定义的类型之间的区别

使用 `clap` 在 Rust CLI 工具中设置布尔标志

为什么 Rust 允许写入不可变的 RwLock?

如果我不想运行析构函数,如何移出具有析构函数的 struct ?

在 Rust 中枚举字符串的最佳方式? (字符()与 as_bytes())

为什么我不能为 Display+Debug 的泛型类型实现 std::error::Error 但有一个不是泛型参数的类型?

相互调用的递归异步函数:检测到循环