RustのFn* trait
Rustにおけるクロージャとは、 Fn
, FnMut
, または FnOnce
を実装した値にすぎない。これらを追ってみた。
Fnはどこから来たか
まず、 Fn/FnMut/FnOnceはキーワード/予約語ではない。syntax::symbol
の一覧にない。
fn main() { let Fn = 0; }
そこで、広く使われている Fn(u8, u8) -> u8
という記法がどのように実現しているかを追う。まず core::ops
によると
#[lang = "fn_once"] #[stable(feature = "rust1", since = "1.0.0")] #[rustc_paren_sugar] #[fundamental] // so that regex can rely that `&str: !FnMut` pub trait FnOnce<Args> { /// The returned type after the call operator is used. #[stable(feature = "fn_once_output", since = "1.12.0")] type Output; /// This is called when the call operator is used. #[unstable(feature = "fn_traits", issue = "29625")] extern "rust-call" fn call_once(self, args: Args) -> Self::Output; }
ここで出てくるattributeは以下の意味がある。
#[lang = "fn_once"]
言語処理系が、fn_once
という名前でこのtraitを発見できるようにする。#[rustc_paren_sugar]
←重要。後述#[fundamental]
トレイト実装の一貫性に関するルールを一時的に緩める。
つまり、 #[rustc_paren_sugar]
というフラグを立てることで特殊な記法を有効化していることがわかる。実際に実験してみると以下のようになる。
// #![feature(unboxed_closures)] // #[rustc_paren_sugar] // trait MyFn<Args> { // type Output; // } trait MyFn2<Args> { type Output; } fn test1<F>(f: F) where F: Fn(u8,u8) -> u8 {} // fn test2<F>(f: F) where F: Fn<(u8,u8), Output=u8> {} // fn test3<F>(f: F) where F: MyFn(u8, u8) -> u8 {} // fn test4<F>(f: F) where F: MyFn<(u8,u8), Output=u8> {} // fn test5<F>(f: F) where F: MyFn2(u8, u8) -> u8 {} fn test6<F>(f: F) where F: MyFn2<(u8,u8), Output=u8> {} fn main() { println!("Hello, world!"); }
上のコードでコメントアウトした部分はunstableで弾かれる。そこで2017/03/08時点のnightlyを使うと上でコメントアウトした部分も含めて全てコンパイルが通る。以下、 Fn*
系traitの内部構造について述べることはunstableであり、今後変更される可能性がある。
括弧記法はどこから来たか
これで、名前に関係なく T(X, Y) -> Z
という形のトレイトがパースされるという予測がたった。実際に探してみると、syntax::parse::parser
の型名をパースする部分にこの記述があった。
これによると、パースの段階では Foo<u8, ()>::Bar(str)::Baz(str) -> [u8]::Quux
のような謎の物体も型名(path)として解釈されるようだ。このうち括弧がついている部分は ast::PathParameters::Parenthesized
という補助的な情報が付与される。
ASTをHIRに落とす処理により、これはhir::ParenthesizedParameters
に変換される。構造はASTのときとほぼ変わらない。
HIRに型をつける段階で convert_parenthesized_parameters
により ()
型引数が <>
型引数に変換される。この時点で <>
との区別はなくなり、以下同様に取り扱われる。
Fnでのみ括弧が使える理由
それではトレイトによって ()
記法が使えたり使えなかったりするのはどこで実現されているか。 #[rustc_paren_sugar]
はrustc_typeck::collect::trait_def_of_item
で処理されている。これを見ると TraitDef
に単純にフラグが立てられているだけだということがわかる。実際の rustc::ty::trait_def::TraitDef::paren_sugar
を見ると、このフラグによる利用制限は一時的なもので、将来的には撤廃する予定と書いてある。
そこで paren_sugar
の利用場面を調べると、rustc_typeck::astconv
の型代入を生成する関数で、以下のことがチェックされていることがわかる。
paren_sugar
が有効なトレイトは、()
記法でのみ利用されている。paren_sugar
が無効なトレイトは、<>
記法でのみ利用されている。
クロージャートレイトのオーバーロード
Fn
トレイトの定義を復習すると、引数の型は型引数、戻り値の型は関連型として扱われていた。つまり、トレイトとしての制約のみ考えると、
- 同じ引数型に対して、複数の戻り値型を割り当てることはできない。
- ただし、複数の異なる引数型を割り当てることはできる。
となるはずである。そこで実験してみると以下のようになる。
fn foo<F: Fn(u8, u8) -> u8 + Fn(u32, u32) -> u32>(f: F) { let x = f(6000, 8000); } fn main() { println!("Hello, world!"); }
error: the type of this value must be known in this context --> src/main.rs:2:13 | 2 | let x = f(6000, 8000); | ^^^^^^^^^^^^^ error[E0059]: cannot use call notation; the first type parameter for the function trait is neither a tuple nor unit --> src/main.rs:2:13 | 2 | let x = f(6000, 8000); | ^^^^^^^^^^^^^ error: aborting due to 2 previous errors error: Could not compile `fn-impl-test`.
このように E0059 が出てしまう。
理由は正確にはわからなかったが、おそらく引数の型を推論できなかったために「タプルでもユニットでもない」と判断されたということのようだ。
まとめ
Fn(X, Y) -> Z
のような記法は、lifetime elisionが行われるなどやや機能的に強力である点を除くと、基本的にトレイトのパラメーター指定の構文糖衣である。ただし、2つの記法の相互互換性にはまだ未確定の仕様が含まれているため、stableでは特定の記法しか利用できない。