この Rust コードは、構造体にライフタイム バインドを設定してコンパイルされるのに、バインドが impl にのみある場合にライフタイム エラーが発生するのはなぜですか?

okwaves2024-01-25  7

最近、次のようなコードを作成しようとしました。

pub struct Foo<'a, F> /* where F: Fn(&u32) -> bool */ {
    u: &'a u32,
    f: F
}

impl<'a, F> Foo<'a, F> 
    where F: Fn(&u32) -> bool 
{
    pub fn new_foo<G: 'static>(&self, g: G) -> Foo<impl Fn(&u32) -> bool + '_>
        where G: Fn(&u32) -> bool 
    {
        Foo { u: self.u, f: move |x| (self.f)(x) && g(x) }
    }
}

ここで、Foo のインスタンスはデータ (u32) の条件を表しており、古いものを消費することなく、new_foo を介してより制限的な Foo をより制限的な Foo から構築できます。ただし、上記のコードは書かれたとおりにコンパイルされず、かなり不可解なエラー メッセージが表示されます。

error[E0308]: mismatched types
 --> src/lib.rs:9:52
  |
9 |     pub fn new_foo<G: 'static>(&self, g: G) -> Foo<impl Fn(&u32) -> bool + '_>
  |                                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
  |
  = note: expected type `std::ops::FnOnce<(&u32,)>`
             found type `std::ops::FnOnce<(&u32,)>`

error: higher-ranked subtype error
  --> src/lib.rs:9:5
   |
9  | /     pub fn new_foo<G: 'static>(&self, g: G) -> Foo<impl Fn(&u32) -> bool + '_>
10 | |         where G: Fn(&u32) -> bool 
11 | |     {
12 | |         Foo { u: self.u, f: move |x| (self.f)(x) && g(x) }
13 | |     }
   | |_____^

error: aborting due to 2 previous errors

多くの実験を行った結果、コードをコンパイルする方法を見つけたので、意図したとおりに機能すると確信しています。私は、境界に依存せずに宣言を記述できる場合、宣言ではなく impls に境界を設定する慣例に慣れていますが、何らかの理由で上記の where 句のコメントを解除する、つまりバインドされた F: Fn(&u32) をコピーします。 -> impl から th までの boolFoo の宣言自体が問題を解決しました。ただし、これがなぜ違いを生むのかはわかりません(そもそもエラーメッセージの意味もよくわかりません)。ここで何が起こっているのか説明できる人はいますか?

正常に機能することを期待して汎用 Fn の代わりにクロージャを使用すると、問題が発生する場合があることがわかりました。通常、引数の型を指定すると役に立ちますが、この場合はそうします。 move |x: &u32| { ... } が役に立ちます。おそらくそれはコンパイラの制限/バグでしょうか?

– ロドリゴ

2020 年 9 月 3 日 10:51



------------------------

Rust に存在する唯一のサブタイプはライフタイムであるため、エラーは (謎めいた形で) 何らかのライフタイムの問題が存在することを示唆しています。さらに、エラーは明らかにクロージャの署名を示しており、これには 2 つのライフタイムが含まれます。

明示的に指定したクロージャ自体の有効期間は、匿名の有効期間より長くなります '_;そして

引数 &u32 の有効期間。これは明示的に指定されていないため、次のように指定したかのように、より上位の有効期間が推測されます。

pub fn new_foo<G: 'static>(&self, g: G) -> Foo<impl for<'b> Fn(&'b u32) -> bool + '_>
    where G: Fn(&u32) -> bool

上記のより明示的な署名を使用すると、(非常に)わずかに役立つエラーが生成されます。


error[E0308]: mismatched types
 --> src/lib.rs:9:52
  |
9 |     pub fn new_foo<G: 'static>(&self, g: G) -> Foo<impl for<'b> Fn(&'b u32) -> bool + '_>
  |                                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
  |
  = note: expected type `std::ops::FnOnce<(&'b u32,)>`
             found type `std::ops::FnOnce<(&u32,)>`

少なくとも、「ある型が他の型より一般的である」ことがわかります。私たちは、任意の存続期間を持つ引数を取ることができるクロージャを期待していましたが、何らかの理由で、Rust は、代わりに次の型を取るクロージャであると考えています。有効期間のより制限された範囲を持つ可能性のある引数。

何が起こったのですか?さて、関数の戻り値は次の式です。

Foo { u: self.u, f: move |x| (self.f)(x) && g(x) }

これはもちろん struct Foo<'a, F> のインスタンスであり、この F は impl ブロッ​​クで宣言されたもの (特性バインド付き) とは関係がありません。実際、構造体定義には F に対する明示的な制限がないため、コンパイラーは式自体からこの型 F を完全に推論する必要があります。構造体定義に特性バインドを与えることにより、コンパイラに Foo のインスタンスを次のように伝えます。上記の式を含めて、for<'b> を実装する F を持ちます。 Fn(&'b u32) -> bool: つまり、&u32 引数の有効期間の範囲には制限がありません。

わかりました。コンパイラは代わりに F を推論する必要があります。実際、コンパイラは Fn(&u32) -> を実装していると推論します。ブール。ただし、&u32 引数が制限される有効期間の範囲を決定するのに十分賢明ではありません。上記の @rodrigo のコメントで示唆されているように、明示的な型アノテーションを追加すると、引数が実際に任意の存続期間を持つことができることがわかります。

クロージャの引数の有効期間に実際に制限がある場合は、'b の定義を上位の特性境界から変更することで、より明示的にそのことを示す必要があります (つまり、戻ります上記のように) 状況に応じて適切なものに変更してください。

Chalk がコンパイラーに完全に統合されれば、制限のない場合と制限された場合の両方でこの推論を実行できるようになると期待されます。それまでの間、コンパイラーは注意を怠り、誤った可能性のある仮定を立てませんでした。ただし、エラーは間違いなくもう少し役立つはずです。

総合生活情報サイト - OKWAVES
総合生活情報サイト - OKWAVES
生活総合情報サイトokwaves(オールアバウト)。その道のプロ(専門家)が、日常生活をより豊かに快適にするノウハウから業界の最新動向、読み物コラムまで、多彩なコンテンツを発信。