クロージャと第一級関数
クロージャ、第一級関数、高階関数は Rust の中核的な要素です。C や C++ には関数ポインタがあります(そして C++ には、私には結局うまく理解できなかった、あの奇妙なメンバー/メソッドポインタのようなものもあります)。しかし、それらは比較的まれにしか使われず、あまり使いやすいものでもありません。C++11 ではラムダが導入されましたが、これは Rust のクロージャにかなり近く、特に実装戦略が非常によく似ています。
まずは、これらについて直感をつかむところから始めたいと思います。その後で、詳細に入っていきます。
関数 foo があるとしましょう: pub fn foo() -> u32 { 42 }。次に、関数を引数として受け取る別の関数 bar を想像してみます(bar のシグネチャは後で示します): fn bar(f: ...) { ... }。C で関数ポインタを渡すのと少し似たように、foo を bar に渡すことができます: bar(foo)。bar の本体では、f を関数であるかのように呼び出せます: let x = f();。
Rust に第一級関数があると言うのは、関数を持ち回ったり、他の値と同じように使ったりできるからです。bar が高階関数であると言うのは、関数を引数として受け取るからです。つまり、関数に対して作用する関数です。
Rust のクロージャは、便利な構文を持つ匿名関数です。クロージャ |x| x + 2 は引数を 1 つ受け取り、それに 2 を足して返します。クロージャの引数に型を与える必要はないことに注意してください(通常は推論できます)。戻り値の型を指定する必要もありません。クロージャ本体を単なる 1 つの式以上にしたい場合は、波括弧を使えます: |x: i32| { let y = x + 2; y }。関数と同じようにクロージャを渡すことができます: bar(|| 42)。
クロージャと他の関数の大きな違いは、クロージャがその環境をキャプチャすることです。これは、クロージャの外側にある変数をクロージャ内から参照できることを意味します。例:
#![allow(unused)] fn main() { let x = 42; bar(|| x); }
クロージャ内で x がスコープ内にあることに注目してください。
これまでにも、イテレータと一緒に使われるクロージャを見てきましたが、これはクロージャの一般的なユースケースです。たとえば、ベクターの各要素に値を加えるには次のようにします。
#![allow(unused)] fn main() { fn baz(v: Vec<i32>) -> Vec<i32> { let z = 3; v.iter().map(|x| x + z).collect() } }
ここで x はクロージャの引数であり、v の各メンバーが x として渡されます。z はクロージャの外側で宣言されていますが、クロージャなので z を参照できます。関数を map に渡すこともできます。
#![allow(unused)] fn main() { fn add_two(x: i32) -> i32 { x + 2 } fn baz(v: Vec<i32>) -> Vec<i32> { v.iter().map(add_two).collect() } }
Rust では関数の内部で関数を宣言することもできる点に注意してください。これらはクロージャではありません。つまり、自分の環境にアクセスできません。単にスコープを限定するための便利機能にすぎません。
#![allow(unused)] fn main() { fn qux(x: i32) { fn quxx() -> i32 { x // エラー: x はスコープ内にありません。 } let a = quxx(); } }
関数型
新しい例の関数を導入しましょう。
#![allow(unused)] fn main() { fn add_42(x: i32) -> i64 { x as i64 + 42 } }
前に見たように、関数を変数に格納できます: let a = add_42;。a の最も正確な型は Rust では書けません。コンパイラがエラーメッセージ内でそれを fn(i32) -> i64 {add_42} と表示するのを見かけることがあります。各関数は、それぞれ固有で匿名の型を持ちます。fn add_41(x: i32) -> i64 は、同じシグネチャを持っていても異なる型です。
より正確でない型を書くことはできます。たとえば、let a: fn(i32) -> i64 = add_42; です。同じシグネチャを持つすべての関数型は、fn 型(プログラマが書ける型)に強制変換できます。
a はコンパイラによって関数ポインタとして表現されます。しかし、コンパイラが正確な型を知っている場合、実際にはその関数ポインタは使いません。a() のような呼び出しは、a の型に基づいて静的にディスパッチされます。コンパイラが正確な型を知らない場合(たとえば、fn 型だけを知っている場合)、呼び出しは値の中の関数ポインタを使ってディスパッチされます。
Fn 型(大文字の 'F' に注意)もあります。これらの Fn 型は、トレイトと同じように境界です(実際、後で見るように、これらはトレイトそのものです)。Fn(i32) -> i64 は、そのシグネチャを持つすべての関数のようなオブジェクトの型に対する境界です。関数ポインタへの参照を取るとき、実際には fat pointer(DST を参照)で表現されるトレイトオブジェクトを作成しています。
関数を別の関数へ渡す場合、または関数をフィールドに格納する場合は、型を書かなければなりません。選択肢はいくつかあり、fn 型か Fn 型のどちらかを使えます。後者の方が優れています。なぜなら、クロージャ(および潜在的には他の関数のようなもの)を含められる一方で、fn 型は含められないからです。Fn 型は動的サイズ型であり、つまり値型として使うことはできません。関数オブジェクトを渡すか、ジェネリクスを使う必要があります。まずはジェネリクスのアプローチを見てみましょう。たとえば:
#![allow(unused)] fn main() { fn bar<F>(f: F) -> i64 where F: Fn(i32) -> i64 { f(0) } }
bar はシグネチャ Fn(i32) -> i64 を持つ任意の関数を受け取ります。つまり、F 型パラメータを任意の関数のような型でインスタンス化できます。bar(add_42) を呼び出して add_42 を bar に渡すことができ、その場合 F は add_42 の匿名型でインスタンス化されます。bar(add_41) を呼び出すこともでき、それも動作します。
bar にクロージャを渡すこともできます。たとえば、bar(|x| x as i64) です。これが動作するのは、クロージャ型も、そのシグネチャに一致する Fn 境界によって境界付けられるからです(関数と同じく、各クロージャはそれぞれ独自の匿名型を持ちます)。
最後に、関数やクロージャへの参照も渡せます: bar(&add_42) または
bar(&|x| x as i64)。
bar を fn bar(f: &Fn(i32) -> i64) ... と書くこともできます。これら 2 つのアプローチ(ジェネリクスと、関数/トレイトオブジェクト)には、かなり異なるセマンティクスがあります。ジェネリクスの場合、bar は単相化されるため、コードが生成されるとき、コンパイラは f の正確な型を知っています。つまり、静的にディスパッチできます。関数オブジェクトを使う場合、関数は単相化されません。f の正確な型は分からないため、コンパイラは仮想ディスパッチを生成しなければなりません。後者は遅くなりますが、前者はより多くのコードを生成します(型パラメータのインスタンスごとに 1 つの単相化された関数)。
実際には、Fn 以外にも関数トレイトがあります。FnMut と FnOnce もあります。これらは Fn と同じように使われます。たとえば、FnOnce(i32) -> i64 です。FnMut は、呼び出すことができ、その呼び出し中に変更され得るオブジェクトを表します。これは通常の関数には当てはまりませんが、クロージャでは、クロージャがその環境を変更できることを意味します。FnOnce は、(多くても)一度だけ呼び出せる関数です。これもまた、クロージャにのみ関係します。
Fn、FnMut、FnOnce はサブトレイト階層にあります。Fn は FnMut です(Fn 関数は変更する権限を持って呼び出しても害がないからです。ただし、その逆は成り立ちません)。Fn と FnMut は FnOnce です(通常の関数が一度だけ呼び出されても害はないからです。ただし、その逆は成り立ちません)。
したがって、高階関数をできるだけ柔軟にするには、Fn 境界ではなく FnOnce 境界を使うべきです(または、関数を複数回呼び出さなければならない場合は FnMut 境界を使います)。
メソッド
メソッドは関数と同じように使用できます。つまり、メソッドへのポインターを取得して
変数に格納する、といったことができます。ドット構文は使用できず、
完全明示の名前付け形式(universal function call syntax の略で UFCS と呼ばれることもあります)を使って、
メソッドを明示的に指定する必要があります。`self` パラメーターはメソッドの最初の引数です。例:
```rust
struct Foo;
impl Foo {
fn bar(&self) {}
}
trait T {
fn baz(&self);
}
impl T for Foo {
fn baz(&self) {}
}
fn main() {
// 固有メソッド。
let x = Foo::bar;
x(&Foo);
// トレイトメソッド。完全明示の名前付け形式に注意。
let y = <Foo as T>::baz;
y(&Foo);
}
ジェネリック関数
ジェネリック関数へのポインターを取得することはできず、ジェネリック関数型を表現する方法もありません。 ただし、すべての型パラメーターがインスタンス化されている場合は、関数への参照を取得できます。例:
fn foo<T>(x: &T) {} fn main() { let x = &foo::<i32>; x(&42); }
ジェネリッククロージャーを定義する方法はありません。多くの型に対して動作するクロージャーが必要な場合は、 トレイトオブジェクト、マクロ(クロージャーを生成するため)、またはクロージャーを返すクロージャーを渡すことができます (返される各クロージャーは異なる型に対して操作できます)。
ライフタイムジェネリック関数と高ランク型
ライフタイムに対してジェネリックな関数型とクロージャーを持つことは可能です。
借用参照を受け取るクロージャーがあるとします。このクロージャーは、参照がどのライフタイムを持っていても 同じように動作できます(実際、コンパイル後のコードではライフタイムは消去されています)。しかし、その型はどのような形になるのでしょうか?
例:
#![allow(unused)] fn main() { fn foo<F>(x: &Bar, f: F) -> &Baz where F: Fn(&Bar) -> &Baz { f(x) } }
ここでの参照のライフタイムは何でしょうか?この単純な例では、 単一のライフタイムを使用できます(ジェネリッククロージャーは不要です)。
#![allow(unused)] fn main() { fn foo<'b, F>(x: &'b Bar, f: F) -> &'b Baz where F: Fn(&'b Bar) -> &'b Baz { f(x) } }
しかし、f が異なるライフタイムを持つ入力に対して動作するようにしたい場合はどうでしょうか?
その場合はジェネリック関数型が必要です。
#![allow(unused)] fn main() { fn foo<'b, 'c, F>(x: &'b Bar, y: &'c Bar, f: F) -> (&'b Baz, &'c Baz) where F: for<'a> Fn(&'a Bar) -> &'a Baz { (f(x), f(y)) } }
ここで新しいのは for<'a> 構文です。これは、ライフタイムに対してジェネリックな関数型を表すために使われます。
これは「すべての 'a について、...」と読みます。理論的には、この関数型は全称量化されています。
上の例では 'a を foo へ持ち上げることはできない点に注意してください。反例:
#![allow(unused)] fn main() { fn foo<'a, 'b, 'c, F>(x: &'b Bar, y: &'c Bar, f: F) -> (&'b Baz, &'c Baz) where F: Fn(&'a Bar) -> &'a Baz { (f(x), f(y)) } }
これはコンパイルされません。なぜなら、コンパイラーが foo の呼び出しに対してライフタイムを推論するとき、
'a に対して単一のライフタイムを選ばなければならないためです。'b と 'c が異なる場合、それはできません。
このようにジェネリックな関数型は高ランク型と呼ばれます。
外側のレベルにあるライフタイム変数はランク 1 を持ちます。上の例の 'a は
外側のレベルへ移動できないため、そのランクは 1 より高くなります。
高ランク関数型の引数を持つ関数の呼び出しは簡単です。コンパイラーがライフタイムパラメーターを推論します。
例: foo(&Bar { ... }, &Bar {...}, |b| &b.field)。
実際のところ、ほとんどの場合、このようなことを気にする必要すらありません。 関数引数上の多くのライフタイムを省略できるのと同じように、コンパイラーは量化されたライフタイムを省略することを許可します。 たとえば、上の例は単に次のように書けます。
#![allow(unused)] fn main() { fn foo<'b, 'c, F>(x: &'b Bar, y: &'c Bar, f: F) -> (&'b Baz, &'c Baz) where F: Fn(&Bar) -> &Baz { (f(x), f(y)) } }
(そして、これは作為的な例なので 'b と 'c が必要なだけです)。
Rust が借用参照を持つ関数型を見ると、通常の省略規則を適用し、 省略された変数をその関数型のスコープで量化します(つまり、高ランクで)。
かなりニッチなユースケースに見えるもののために、なぜこれほどの複雑さが必要なのか疑問に思うかもしれません。 本当の動機は、外側の関数によって提供されるデータに対して操作するための関数を受け取る関数です。例:
#![allow(unused)] fn main() { fn foo<F>(f: F) where F: Fn(&i32) // 完全明示の型: for<'a> Fn(&'a i32) { let data = 42; f(&data) } }
このような場合、私たちには高ランク型が必要です。代わりに foo にライフタイムパラメーターを追加した場合、
正しいライフタイムを推論することは決してできません。その理由を見るために、どのように動作し得るかを見てみましょう。
fn foo<'a, F: Fn(&'a i32)> ... を考えます。Rust では、任意のライフタイムパラメーターは、
それが宣言されているアイテムより長く生存しなければなりません(そうでなければ、そのライフタイムを持つ引数が、
その関数内で使用される可能性がありますが、そこで生存していることは保証されません)。foo の本体では
f(&data) を使用します。その参照に対して Rust が推論するライフタイムは、(長くても)
data が宣言された場所からスコープを外れる場所まで続きます。'a は
foo より長く生存しなければなりませんが、その推論されたライフタイムはそうではないため、
この方法で f を呼び出すことはできません。
しかし、高ランクライフタイムを使うと、f は任意のライフタイムを受け入れられるため、
&data から来る匿名のライフタイムでも問題なく、関数は型チェックに通ります。
列挙型コンストラクター
これは少し脱線ですが、ときには便利なテクニックです。列挙型のすべてのバリアントは、 そのバリアントのフィールドから列挙型への関数を定義します。例:
#![allow(unused)] fn main() { enum Foo { Bar, Baz(i32), } }
これは 2 つの関数、Foo::Bar: Fn() -> Foo と Foo::Baz: Fn(i32) -> Foo を定義します。
通常、私たちはバリアントをこのような方法では使わず、関数ではなくデータ型として扱います。
しかし、たとえば i32 のリストがある場合、次のようにして Foo のリストを作成できるので便利なことがあります。
#![allow(unused)] fn main() { list_of_i32.iter().map(Foo::Baz).collect() }
クロージャーの種類
クロージャーには 2 種類の入力があります。明示的に渡される引数と、 環境からキャプチャーする変数です。通常、どちらの入力についてもすべて推論されますが、 必要であればより細かく制御できます。
引数については、Rust に推論させる代わりに型を宣言できます。
戻り値の型を宣言することもできます。|x| { ... } と書く代わりに、
|x: i32| -> String { ... } と書けます。引数が所有されるか借用されるかは、
型(宣言されたもの、または推論されたもの)によって決まります。
キャプチャーされる変数については、型はほとんど環境から分かりますが、 Rust は少し追加の魔法を行います。変数は参照でキャプチャーされるべきでしょうか、それとも値でキャプチャーされるべきでしょうか? Rust はこれをクロージャーの本体から推論します。可能であれば、Rust は参照でキャプチャーします。例:
#![allow(unused)] fn main() { fn foo(x: Bar) { let f = || { ... x ... }; } }
すべてがうまくいけば、f の本体では、x は &Bar 型になり、そのライフタイムは
foo のスコープによって制限されます。しかし、x が変更される場合、Rust は
キャプチャーが可変参照によるもの、つまり x の型が &mut Bar であると推論します。
x が f の中でムーブされる場合(たとえば、値型を持つ変数やフィールドに格納される場合)、
Rust はその変数が値でキャプチャーされなければならない、つまり Bar 型を持つと推論します。
これはプログラマーが上書きできます(クロージャをフィールドに格納したり、関数から返したりする場合には必要になることがあります)。クロージャの前に move キーワードを使用します。すると、キャプチャされたすべての変数は値でキャプチャされます。たとえば、let f = move || { ... x ... }; では、x は常に Bar 型になります。
以前、さまざまな関数の種類、すなわち Fn、FnMut、FnOnce について話しました。これで、なぜそれらが必要なのかを説明できます。クロージャでは、可変性と一回性はキャプチャされた変数を指します。クロージャが、キャプチャする変数のいずれかを変更する場合、そのクロージャは FnMut 型になります(これはコンパイラーによって完全に推論されるため、注釈は不要です)。変数がクロージャにムーブされる場合、つまり値でキャプチャされる場合(明示的な move による場合でも、推論による場合でも)、そのクロージャは FnOnce 型になります。そのようなクロージャを複数回呼び出すのは安全ではありません。キャプチャされた変数が複数回ムーブされることになるためです。
Rust は可能な限り、そのクロージャに対して最も柔軟な型を推論しようとします。
実装
クロージャは匿名構造体として実装されます。その構造体には、クロージャによってキャプチャされた各変数に対応するフィールドがあります。これはライフタイムに関してパラメトリックであり、キャプチャされた変数のライフタイムに対する境界となる単一のライフタイムパラメーターを持ちます。この匿名構造体は、クロージャを実行するために呼び出される call メソッドを実装します。
たとえば、次を考えてみます。
fn main() { let x = Foo { ... }; let f = |y| x.get_number() + y; let z = f(42); }
コンパイラーはこれを次のように扱います。
struct Closure14<'env> { x: &'env Foo, } // 実際にはこのようには実装されていません。以下を参照してください。 impl<'env> Closure14<'env> { fn call(&self, y: i32) -> i32 { self.x.get_number() + y } } fn main() { let x = Foo { ... }; let f = Closure14 { x: x } let z = f.call(42); }
上で述べたように、関数トレイトには Fn、FnMut、FnOnce の 3 種類があります。実際には、call メソッドは固有の impl に含まれるのではなく、これらのトレイトによって要求されます。Fn には self を参照で受け取る call メソッドがあり、FnMut には self を可変参照で受け取る call_mut があり、FnOnce には self を値として受け取る call_once があります。
上で見てきた関数型は Fn(i32) -> i32 のような形で、トレイト型にはあまり見えません。ここには少し魔法があります。Rust はこの丸括弧の糖衣構文を関数型に対してのみ許可しています。通常の型(「山括弧型」)に脱糖するには、引数の型はタプル型として扱われ、型パラメーターとして渡され、戻り値の型は Output という関連型として扱われます。したがって、Fn(i32) -> i32 は Fn<(i32,), Output=i32> に脱糖され、Fn トレイトの定義は次のようになります。
#![allow(unused)] fn main() { pub trait Fn<Args> : FnMut<Args> { fn call(&self, args: Args) -> Self::Output; } }
したがって、上の Closure14 の実装は、より実際には次のような形になります。
#![allow(unused)] fn main() { impl<'env> FnOnce<(i32,)> for Closure14<'env> { type Output = i32; fn call_once(self, args: (i32,)) -> i32 { ... } } impl<'env> FnMut<(i32,)> for Closure14<'env> { fn call_mut(&mut self, args: (i32,)) -> i32 { ... } } impl<'env> Fn<(i32,)> for Closure14<'env> { fn call(&self, args: (i32,)) -> i32 { ... } } }
関数トレイトは core::ops で確認できます。
上では、ジェネリクスを使用すると静的ディスパッチになり、トレイトオブジェクトを使用すると仮想ディスパッチになることについて話しました。これで、その理由をもう少し詳しく見ることができます。
call を呼び出すとき、それは静的にディスパッチされるメソッド呼び出しであり、仮想ディスパッチはありません。モノモーフィゼーションされた関数に渡す場合でも、型は依然として静的に分かっており、静的ディスパッチが行われます。
クロージャをトレイトオブジェクトにすることもできます。たとえば、&Fn(i32)->i32 型または Box<Fn(i32)->i32> 型を持つ &f や Box::new(f) です。これらはポインター型であり、トレイトへのポインター型であるため、そのポインターはファットポインターです。つまり、それらはデータそのものへのポインターと、vtable へのポインターで構成されます。vtable は call(または call_mut など)のアドレスを検索するために使用されます。
クロージャのこれら 2 つの表現は、boxed クロージャおよび unboxed クロージャと呼ばれることがあります。unboxed クロージャは、静的ディスパッチを伴う値渡しのバージョンです。boxed バージョンは、動的ディスパッチを伴うトレイトオブジェクト版です。昔の Rust には boxed クロージャしかありませんでした(そしてそのシステムはかなり異なっていました)。
参考資料
FIXME: C++ 11 のクロージャとの関連付け