Chapter 16

クロージャ

📌 クロージャとは

クロージャ とは,簡単に言うと,変数に束縛できたり,関数の引数として渡すことのできる名前のない関数(無名関数)のことです.クロージャはその呼び出し元のスコープにある変数を キャプチャ することも出来ます.厳密に言うと,無名関数の中で束縛していない変数のことを自由変数と言い,自由変数をまとめた環境を無名関数のスコープ内に閉じこめたものをクロージャと呼びます.

クロージャは || で定義します.引数があれば |param1, param2| のように || の間に入れます.その後に {} で本体を記述します.本体が式1つだけなら {} を省略することが出来ます.次のコードは関数とそれと同じ振る舞いをするクロージャの例です.

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

クロージャはそれぞれ独自(unique)の型を持っています.クロージャは Fn, FnMut, FnOnce トレイトのどれかのインスタンスです.それぞれ &self, &mut self, self を内部的に引数として受け取っているかどうかの違いがあります.また, FnFnMut を, FnMutFnOnce を継承しています.

自由変数をまとめた環境をどのように扱うかで,どのトレイトのインスタンスになるかが決まります.まず,すべてのクロージャは必ず FnOnce のインスタンスになります.そして,無名関数の中で環境から所有権を移動することがなければ(可変参照は出来る), FnMut のインスタンスになります.さらに,環境を変更しないのであれば,不変参照となるので Fn のインスタンスになります.

自由変数が環境にまとめられるとき,自由変数が束縛しているオブジェクトがコピートレイトのインスタンスであれば,コピーが作成されます.もし,自由変数をクロージャの中だけで使用するということが分かっているならば,環境にまとめられるときに,コピーではなく所有権を移動することが出来ます.それを行うには,クロージャの前に move を指定します.

📌 Fn と fn

Fn トレイトと fn というキーワードは別ものです. fn は関数定義で使いますが, fn は型でもあります.そして, fn のことを 関数ポインタ と言います. fnFn のインスタンスなので, FnMut, FnOnce のインスタンスでもあります.

📌 Sized トレイト

Rust は静的型付け言語です.静的型の特徴の1つとして,型のサイズがコンパイル時に分かることです.しかし,コンパイル時にサイズがわからないこともあります.Rust は型のサイズが分かっているとき,自動でその型を Sized トレイトのインスタンスにします.Rust は型が Sized トレイトのインスタンスであることを仮定し,それを制約します.つまり,関数の引数などは Sized トレイトのインスタンスでなければなりません.ジェネリック型も同じです.それに対して,例えば, str 型は実行時にサイズが決まるので,そのような型のことを 動的サイズ型 (Dynamically sized types: DST)といいます.

トレイトは Sized トレイトの対象になりません.トレイトはデータ型の分類の仕組みであり,サイズは考慮していないからです.ここで,クロージャに話を戻します.クロージャはトレイトで実装されているので,コンパイル時にサイズがわからないのです.例えば,次のようにクロージャを返す関数はエラーになります.

fn returns_closure() -> Fn(i32) -> i32 {
    |x| x + 1
}
// error[E0277]: the trait bound `std::ops::Fn(i32) -> i32 + 'static:
// std::marker::Sized` is not satisfied

このような動的サイズ型をどうすれば Sized トレイトのインスタンスにすることができるかということですが,参照( & )にするか, Box にするかです.例えば, Box を使えばクロージャを次のように返すことが出来ます.

fn returns_closure() -> Box<Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

既に述べたように Rust は引数の型が Sized であることを制約します.例えば次のようなコードがあります.

fn put<T: std::fmt::Debug>(a: &T) {
    println!("{:?}", a);
}

fn main() {
    put("hoge");
}

この T は文字列スライスである str で,関数 put の引数は参照で受け取っています. str は動的サイズ型なので参照を付けていますが,これはエラーになってしまいます.これは暗黙的に T: Sized となっているためで,引数側に参照を付けても TSized でなければなりません.しかし,引数側で参照を付けているので,コードとしては動的サイズ型を指定しても問題ありません.そこで, T が動的サイズ型を受け入れられるように ?Sized を指定するこができます.

fn put<T: std::fmt::Debug + ?Sized>(a: &T) {
    println!("{:?}", a);
}

fn main() {
    put("hoge");
}

これでコンパイルが通ります.

📌 参照を返す関数

str 型は動的サイズ型です.このままでは Sized にならないので,文字列スライスは &str 型です.クロージャを返すところでは参照を使わずに Box を使いました.参照を使っても返すことができるのですが,関数から参照を返すときには ライフタイム (lifetime)というものを考慮しなければなりません.個人的にこのライフタイムは余程の理由がない限り扱うべきでないものと思っているので,本書では詳しく扱いません.なので,関数から参照を返すのは可能なかぎり避けましょう.また,構造体にも参照のフィールドを持つことも出来ますが,こちらもライフタイムが必要になってしまいます.

📌 静的と動的

Rust は静的型付け言語にもかかわらず,動的サイズ型もサポートしているのが強みでもあります.これにより静的ディスパッチおよび動的ディスパッチの両方を実現することが出来ます.動的サイズ型は参照や Box を使うことで扱えることがわかりました.クロージャはトレイトであり,動的サイズ型であり,参照や Box を使うことでオブジェクトとして扱えるようになります.このようなオブジェクトを トレイトオブジェクト といいます.

トレイトオブジェクトは,トレイトのメソッドのみ呼び出せることになります.トレイトオブジェクトは,そのトレイトのインスタンスであればどの型のオブジェクトでも置き換えることができます.このように,トレイトオブジェクトを扱う側は実際のオブジェクトの型を知らなくても,そのメソッドを呼び出せるということ,そしてオブジェクトの型によってメソッドの動作を変えることが出来ることになります.これらの仕組みを 動的ディスパッチ といいます.

ジェネリック型や impl Trait で指定した型はコンパイル時に型が決まりますので静的です.この Trait には任意のトレイト名を指定します.トレイトの型は Sized ではないので,参照や Box で指定する必要があるのですが,静的である impl Trait と区別しやすいように,動的であることを明示する dyn が導入され, dyn Trait という型を使います.よって, &dyn Trait&mut dyn Trait, Box<dyn Trait> という形で利用します.

ここでクロージャを返す関数を振り返ってみます.動的と静的を区別するために dyn を指定するのですが,これは後から導入されたものなので,省略してもコンパイルは通ります.ただし,警告で付けるように促されるので,実際は次のようになります:

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

実は impl を使うと簡単に静的として扱えます:

fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}