如何组合函数以修改Rust中的结构体

huangapple go评论60阅读模式
英文:

How to compose functions that modify a struct in Rust

问题

以下是您要翻译的代码部分:

Take a super-simple struct Foo:

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

和我找到的一个compose宏这里:

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 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); // 打印 6
    y.a = 7;
    println!("{:?}", x); // 打印 Foo { a: 7 }
}

问题出现在当我尝试定义第二个简单的函数并将它们组合时:

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

我得到一个错误,即主函数中的可变借用 let y = baz(&mut x); 不活得够长。我认为我不够理解这个compose宏,不理解出了什么问题。

此外,当我在第一个版本中打印与 x 绑定的结构体时,它有效,因为它在对可变借用 y 的最后使用之后,因此我可以对 x 进行不可变借用来打印它。但是在第二个版本中,如果我尝试在最后打印 x,它会说它仍然被可变借用。Compose宏中的某些东西似乎在“保留”对 x 的可变借用?如何让这个工作?能够让这个工作吗?

Playground

根据评论进行编辑:

似乎虽然 compose_two 中的闭包实际上并没有保留对结构体的可变引用,但返回类型并没有指定它不会(闭包关闭捕获的变量,对吗?),所以编译器被迫假设它可能会。如何说服编译器我没有保留那个引用?


<details>
<summary>英文:</summary>

Take a super-simple struct Foo:

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


and a compose macro I got [here](https://functional.works-hub.com/learn/functional-programming-jargon-in-rust-1b555):

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


I can define a simple function that takes a mutable reference and modifies the struct and returns the reference it was handed:

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


and it works as expected:

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


The problem comes when I try to define a second simple function and compose the two:

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


I get an error that the mutable borrow of x in main `let y = baz(&amp;mut x);` doesn&#39;t live long enough. I don&#39;t think I understand that compose macro well enough to understand what&#39;s gong wrong.

Also when I print the struct bound to `x` in the first version it works because it&#39;s after the last use of the mutable borrow `y` so I can immutably borrow `x` to print it. But in the second version if I try to print `x` at the end it says it&#39;s still borrowed mutably. Something in the compose macro seems to be &quot;holding on&quot; to that mutable borrow of `x`?

How do I make this work? *Can* this be made to work?

[Playground](https://play.rust-lang.org/?version=stable&amp;mode=debug&amp;edition=2021&amp;gist=c1304dbe8e0b5cc93752b4bb77431aaa)

# Edit based on comments:

It seems that while the closure in `compose_two` doesn&#39;t actually hold on to the mutable reference to the struct the return type doesn&#39;t specify that it doesn&#39;t (closures *close over* captured variables right?), and so the compiler is forced to assume that it might. How do I convince the compiler that I&#39;m not holding that reference?

</details>


# 答案1
**得分**: 3

这段代码讨论了在Rust中处理函数组合的问题,以及如何解决一些潜在的编译器错误。

请注意,这部分内容没有涉及具体的翻译,而是对代码和问题的解释和讨论。如果您有特定的问题或需要翻译特定的部分,请提供相关的具体内容,我将尽力协助您。

<details>
<summary>英文:</summary>

&gt; _Can_ this be made to work?

No. But depending on your use-case, maybe yes.

You can make it work if either:

 - You can constrain the function types to be `Copy` (if you are using function items (`fn`), they are always `Copy`, but for closures this may be a problem if they capture non-`Copy` types).
 - You can use nightly.
 - You can change the user of `compose!()` (`main()`).
 - You can limit `compose!()` to references (mutable references, to be precise, but you can make a version for shared references too. Of course, if you want to make separate versions for references and owned types, this is fine).

There are three factors here, that joined together they convince the compiler `x` can be used after its lifetime. If we break one of them, it will work. Two of them are actually false, but the compiler doesn&#39;t know that (or doesn&#39;t want to rely on that). The factors are:

 1. The compiler believes that the returned closure can capture its parameter. This is false, as I will explain in a minute, but the compiler does not know that.
 2. The compiler believes the closure has a drop implementation, and can use `x` (captured in step 1) inside this drop. In fact, the compiler knows it doesn&#39;t, but because we used `impl Trait`, it is forced to treat it as if it implemented drop, so it will not be a breaking change to add one.
 3. `x` is dropped before `baz`. This is true (variables are dropped in reversed order to their declaration), and combined with the two previous beliefs of the compiler it means that when `baz` will (potentially) use its captured `x` in its drop, it will be after `x`&#39;s lifetimes.

Let&#39;s start with the last claim. It is the easiest to break, because you only need to swap the order of `x` and `baz`:
```rust
fn main() {
    let mut x = 3;
    let baz = compose_two(foo, bar);
    let y = baz(&amp;mut x);
    println!(&quot;{:?}&quot;, y);
}

But it is not always possible to change main(), or it may not be possible to declare x before baz.

So let's return to the second claim. The compiler believes the closure has a Drop impl because it is in impl Trait. What if it would not be?

Unfortunately, this requires nightly, because writing closures manually requires the features fn_traits and unboxed_closures. But it is definitely possible (and a nice side benefit is that the function can be conditionally FnOnce/FnMut/Fn depending on what its input functions are):

#![feature(fn_traits, unboxed_closures)]

struct ComposeTwo&lt;G, F&gt;(F, G);

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

    extern &quot;rust-call&quot; fn call_once(self, (x,): (A,)) -&gt; Self::Output {
        (self.1)((self.0)(x))
    }
}

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

impl&lt;A, B, C, G, F&gt; std::ops::Fn&lt;(A,)&gt; for ComposeTwo&lt;G, F&gt;
where
    F: Fn(A) -&gt; B,
    G: Fn(B) -&gt; C,
{
    extern &quot;rust-call&quot; fn call(&amp;self, (x,): (A,)) -&gt; Self::Output {
        (self.1)((self.0)(x))
    }
}

fn compose_two&lt;G, F&gt;(f: F, g: G) -&gt; ComposeTwo&lt;G, F&gt; {
    ComposeTwo(f, g)
}

Another way to break this assumption is by making the returned closure Copy. Copy type can never implement Drop, and the compiler knows that, and assumes they don't. Unfortunately, because the closure captures f and g, they need to be Copy too:

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

The last way is the most complicated to explain. First, I need to explain why the compiler thinks the closure can capture x, while in fact it cannot.

Let's first think why the closure cannot do that: what lifetime will it put in place of the &#39;? below?

struct Closure {
    f: some_function_type,
    g: some_function_type,
    captured_x: Option&lt;&amp;&#39;? mut Foo&gt;,
}

When baz was defined (where we must decide what lifetime we'll use), we still don't know what will be passed to the closure, and so we don't know what lifetime we should use!

This knowledge, which is essentially "the closure can be called with any lifetime", is passed through Higher-Ranked Trait Bounds (HRTB) in Rust, spelled for&lt;&#39;lifetime&gt;. So, A in compose_two() should've been HRTB.

But here lies the problem: generic parameters cannot be HRTB. They must be instantiated with a concrete lifetime. So, the compiler chooses some lifetime &#39;x for baz, and this lifetime must be bigger than baz itself - otherwise it would contain a dangling lifetime - and therefore it theoretically could have a member with that lifetime, and so the compiler believes baz can store the reference to x, while in reality it cannot.

If only we could make it HRTB...

We can! If we does not make it completely generic, and instead specify it as a reference:

fn compose_two&lt;A, B, C, G, F&gt;(f: F, g: G) -&gt; impl for&lt;&#39;a&gt; Fn(&amp;&#39;a mut A) -&gt; &amp;&#39;a mut C
where
    F: for&lt;&#39;a&gt; Fn(&amp;&#39;a mut A) -&gt; &amp;&#39;a mut B,
    G: for&lt;&#39;a&gt; Fn(&amp;&#39;a mut B) -&gt; &amp;&#39;a mut C,
{
    move |x| g(f(x))
}

Or, using elided form, since HRTB is the default for Fn trait bounds:

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

It unfortunately also requires B: &#39;static, because the compiler cannot conclude B will live long enough (another limitation of the language), but then it works!

fn compose_two&lt;A, B: &#39;static, C, G, F&gt;(f: F, g: G) -&gt; impl Fn(&amp;mut A) -&gt; &amp;mut C
where
    F: Fn(&amp;mut A) -&gt; &amp;mut B,
    G: Fn(&amp;mut B) -&gt; &amp;mut C,
{
    move |x| g(f(x))
}

答案2

得分: 1

以下是您要求的代码部分的中文翻译:

首先,使用rust-analyzer的快速修复功能(在vscode中按ctrl+.)可以帮助内联宏几次:

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

在这种情况下,它只是调用给定的函数compose_two。有关错误的完整编译器诊断如下:

error[E0597]: `x` does not live long enough
  --> src/main.rs:34:17
   |
34 |     let y = baz(&mut x);
   |                 ^^^^^^ borrowed value does not live long enough
35 |     println!("{:?}", y.a);
36 | }
   | -
   | |
   | `x` dropped here while still borrowed
   | borrow might be used here, when `baz` is dropped and runs the destructor for type `impl Fn(&mut Foo) -> &mut Foo`
   |
   = note: values in a scope are dropped in the opposite order they are defined

我不完全确定发生了什么,但我认为问题在于编译器无法推断可变引用在调用baz后是否会被使用。如果您内联调用compose_two,它就能正常工作:

let baz = move |x| bar(foo(x));

如果您将baz定义为一个独立的函数,它也可以正常工作:

fn baz(x: &mut Foo) -> &mut Foo{
    compose_two(foo, bar)(x)
}

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

但我不知道为什么编译器无法从baz(变量)的impl Fn(&mut Foo) -> &mut Foo类型中推断出这一点。

编辑:问题评论中的一些讨论为此提供了一些启发。正如Chayim Friedman所描述的,编译器无法从impl Fn(&mut Foo) -> &mut Foo类型中假定太多,因此它会假定它可能会保持引用(但实际上并不会)。这就是为什么如果您内联闭包或将baz提取到自己的函数中,编译器会有更多信息,因此它可以正常工作。

在这种情况下,可以通过首先声明x,以便在baz(编译器认为它持有对x的可变引用)被丢弃之前,将其修复为:

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

First, it helps to inline the macro a couple of times using the rust-analyzer quick fix (keybind ctrl+. in vscode):

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

In this case it just calls compose_two with the given functions. The full compiler diagnostic for the error is:

error[E0597]: `x` does not live long enough
  --&gt; src/main.rs:34:17
   |
34 |     let y = baz(&amp;mut x);
   |                 ^^^^^^ borrowed value does not live long enough
35 |     println!(&quot;{:?}&quot;, y.a);
36 | }
   | -
   | |
   | `x` dropped here while still borrowed
   | borrow might be used here, when `baz` is dropped and runs the destructor for type `impl Fn(&amp;mut Foo) -&gt; &amp;mut Foo`
   |
   = note: values in a scope are dropped in the opposite order they are defined

I'm not entirely sure what's going on here but I think the problem is that the compiler can't infer that the mutable reference isn't used after the call to baz for some reason. It works if you inline the call to compose_two:

    let baz = move |x| bar(foo(x));

It works if you define baz as a separate function:

fn baz(x: &amp;mut Foo) -&gt; &amp;mut Foo{
    compose_two(foo, bar)(x)
}

fn main() {
    let bazz = baz;
    let mut x = Foo { a: 3 };
    let y = bazz(&amp;mut x);
    println!(&quot;{:?}&quot;, y.a);
}

But I don't know why it can't infer that from the impl Fn(&amp;mut Foo) -&gt; &amp;mut Foo type of baz (the variable).

Edit: some discussion in the question's comments has shed some light here. As Chayim Friedman described, the compiler can't assume much from the impl Fn(&amp;mut Foo) -&gt; &amp;mut Foo type so it assumes it could be holding the reference (but it isn't). Which is why it works if you inline the closure or extract baz to it's own function, because then the compiler has more information.

In this case, it can be fixed by declaring x first so that baz (which the compiler thinks is holding a mutable reference to x) is dropped before x:

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

huangapple
  • 本文由 发表于 2023年2月23日 20:35:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/75544917.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定