クロージャ
Rust のクロージャは、変数に保存したり、他の関数に引数として渡したりできる無名関数です。ある場所でクロージャを作成し、その後、別の場所でそのクロージャを呼び出して、異なるコンテキストで評価できます。関数とは異なり、クロージャは定義されたスコープから値をキャプチャできます。ここでは、こうしたクロージャの機能によって、コードの再利用や振る舞いのカスタマイズがどのように可能になるかを示します。
環境のキャプチャ
まず、クロージャを使って、定義された環境から値をキャプチャし、あとで使う方法を見ていきましょう。状況は次のとおりです。私たちの T シャツ会社では、販促のために、ときどきメーリングリストの登録者の誰かに限定版の特別なシャツを無料で配布しています。メーリングリストの登録者は、任意でプロフィールに好きな色を追加できます。無料シャツの当選者に好きな色が設定されていれば、その色のシャツを受け取ります。好きな色を指定していなければ、その時点で会社に最も多く在庫がある色のシャツを受け取ります。
これを実装する方法はたくさんあります。この例では、簡単にするため、利用可能な色数を制限して、Red と Blue というバリアントを持つ ShirtColor という enum を使います。会社の在庫は Inventory 構造体で表し、その shirts という名前のフィールドには、現在在庫にあるシャツの色を表す Vec<ShirtColor> が入っています。Inventory に定義された giveaway メソッドは、無料シャツ当選者の任意の色の希望を受け取り、その人が受け取るシャツの色を返します。この構成をリスト 13-1 に示します。
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
Red,
Blue,
}
struct Inventory {
shirts: Vec<ShirtColor>,
}
impl Inventory {
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
user_preference.unwrap_or_else(|| self.most_stocked())
}
fn most_stocked(&self) -> ShirtColor {
let mut num_red = 0;
let mut num_blue = 0;
for color in &self.shirts {
match color {
ShirtColor::Red => num_red += 1,
ShirtColor::Blue => num_blue += 1,
}
}
if num_red > num_blue {
ShirtColor::Red
} else {
ShirtColor::Blue
}
}
}
fn main() {
let store = Inventory {
shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
};
let user_pref1 = Some(ShirtColor::Red);
let giveaway1 = store.giveaway(user_pref1);
println!(
"The user with preference {:?} gets {:?}",
user_pref1, giveaway1
);
let user_pref2 = None;
let giveaway2 = store.giveaway(user_pref2);
println!(
"The user with preference {:?} gets {:?}",
user_pref2, giveaway2
);
}
main で定義されている store には、この限定版プロモーション用に配布するシャツとして、青いシャツが 2 枚、赤いシャツが 1 枚残っています。giveaway メソッドを、赤いシャツを希望するユーザーと、希望をまったく持たないユーザーに対して呼び出します。
繰り返しになりますが、このコードはさまざまな方法で実装できます。ここではクロージャに焦点を当てるため、すでに学んだ概念に絞っています。例外は、クロージャを使っている giveaway メソッドの本体だけです。giveaway メソッドでは、ユーザーの希望を Option<ShirtColor> 型の引数として受け取り、user_preference に対して unwrap_or_else メソッドを呼び出します。Option<T> の unwrap_or_else メソッドは標準ライブラリで定義されています。これは 1 つの引数を取ります。引数は、引数をまったく取らず、値 T を返すクロージャです(この場合の T は Option<T> の Some バリアントに格納される型であり、ShirtColor です)。Option<T> が Some バリアントであれば、unwrap_or_else は Some の中の値を返します。Option<T> が None バリアントであれば、unwrap_or_else はクロージャを呼び出し、そのクロージャが返した値を返します。
unwrap_or_else への引数として、クロージャ式 || self.most_stocked() を指定します。これは、それ自体では何の引数も取らないクロージャです(クロージャに引数がある場合は、2 本の縦棒の間に書かれます)。クロージャの本体は self.most_stocked() を呼び出します。ここでクロージャを定義しており、unwrap_or_else の実装は、結果が必要になったときにあとでこのクロージャを評価します。
このコードを実行すると、次のように出力されます。
$ cargo run
Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue
ここで興味深い点の 1 つは、現在の Inventory インスタンスに対して self.most_stocked() を呼び出すクロージャを渡していることです。標準ライブラリは、私たちが定義した Inventory や ShirtColor 型についても、この場面で使いたいロジックについても、何も知る必要がありませんでした。クロージャは self である Inventory インスタンスへの不変参照をキャプチャし、私たちが指定したコードとともにそれを unwrap_or_else メソッドに渡します。一方、関数はこのように環境をキャプチャすることはできません。
クロージャ型の推論と注釈
関数とクロージャの違いは、ほかにもあります。クロージャでは通常、fn 関数のように、引数や戻り値の型を注釈する必要はありません。関数では、型がユーザーに公開される明示的なインターフェースの一部なので、型注釈が必要です。このインターフェースを厳密に定義することは、関数がどの型の値を使い、どの型の値を返すのかについて、全員の認識を一致させるために重要です。一方、クロージャはこのような公開インターフェースでは使われません。クロージャは変数に保存され、名前を付けてライブラリの利用者に公開することなく使われます。
クロージャは通常短く、任意の場面で使われるというより、狭い文脈の中でのみ意味を持ちます。こうした限定された文脈では、コンパイラはほとんどの変数の型を推論できるのと同じように、引数の型や戻り値の型を推論できます(コンパイラがクロージャの型注釈も必要とするまれな場合はあります)。
変数と同様に、厳密には必要なくても、より冗長になる代わりに明示性と明確さを高めたいなら、型注釈を追加できます。クロージャに型注釈を付けると、リスト 13-2 に示す定義のようになります。この例では、リスト 13-1 のように引数として渡す位置でクロージャを定義するのではなく、クロージャを定義して変数に格納しています。
use std::thread;
use std::time::Duration;
fn generate_workout(intensity: u32, random_number: u32) {
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
if intensity < 25 {
println!("Today, do {} pushups!", expensive_closure(intensity));
println!("Next, do {} situps!", expensive_closure(intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_closure(intensity)
);
}
}
}
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(simulated_user_specified_value, simulated_random_number);
}
型注釈を追加すると、クロージャの構文は関数の構文とより似たものに見えます。ここでは比較のために、引数に 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 ;
最初の行は関数定義を示し、2 行目は型注釈を完全に付けた
クロージャ定義を示しています。3 行目では、クロージャ定義から型注釈を
取り除いています。4 行目では、中括弧を取り除いています。これは、
クロージャ本体が式を 1 つだけ持つため省略可能です。これらはすべて
有効な定義であり、呼び出されたときに同じ振る舞いをします。
add_one_v3 と add_one_v4 の行は、コンパイルできるようにするために
クロージャが評価される必要があります。これは、型がその使用方法から
推論されるためです。これは、let v = Vec::new(); で、Rust が型を
推論できるようにするには、型注釈を付けるか、何らかの型の値を Vec に
挿入する必要があるのと似ています。
クロージャ定義では、コンパイラは各パラメータと戻り値に対して 1 つの
具体的な型を推論します。たとえば、リスト 13-3 は、パラメータとして
受け取った値をそのまま返すだけの短いクロージャの定義を示しています。
このクロージャは、この例の目的以外ではあまり有用ではありません。
この定義には型注釈をまったく追加していないことに注目してください。
型注釈がないため、このクロージャはどの型でも呼び出すことができ、
ここでは最初に String でそれを行っています。その後、
example_closure を整数で呼び出そうとすると、エラーになります。
fn main() {
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
}
コンパイラは次のエラーを出します。
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
--> src/main.rs:5:29
|
5 | let n = example_closure(5);
| --------------- ^ expected `String`, found integer
| |
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
--> src/main.rs:4:29
|
4 | let s = example_closure(String::from("hello"));
| --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src/main.rs:2:28
|
2 | let example_closure = |x| x;
| ^
help: try using a conversion method
|
5 | let n = example_closure(5.to_string());
| ++++++++++++
For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error
最初に String 値で example_closure を呼び出したとき、コンパイラは
x の型とクロージャの戻り値の型を String だと推論します。すると、
それらの型は example_closure 内のクロージャに固定され、次に同じ
クロージャで別の型を使おうとすると型エラーになります。
参照をキャプチャする、または所有権をムーブする
クロージャは、その環境から値を 3 つの方法でキャプチャできます。これらは、 関数がパラメータを受け取る 3 つの方法、すなわち不変借用、可変借用、 所有権の取得に直接対応しています。クロージャは、関数本体がキャプチャ した値をどのように使うかに基づいて、どの方法を使うかを決定します。
リスト 13-4 では、list という名前のベクタへの不変参照をキャプチャする
クロージャを定義しています。これは、値を出力するために不変参照だけが
必要だからです。
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let only_borrows = || println!("From closure: {list:?}");
println!("Before calling closure: {list:?}");
only_borrows();
println!("After calling closure: {list:?}");
}
この例は、変数をクロージャ定義に束縛できること、そして後でその変数名と 丸括弧を使って、まるでその変数名が関数名であるかのようにクロージャを 呼び出せることも示しています。
同時に list への不変参照を複数持てるため、list にはクロージャ定義の
前のコードからも、クロージャ定義の後でクロージャが呼び出される前からも、
そしてクロージャが呼び出された後からも引き続きアクセスできます。この
コードはコンパイルされ、実行され、次のように出力されます。
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]
次に、リスト 13-5 では、クロージャ本体を変更して list ベクタに要素を
追加するようにします。これにより、クロージャは可変参照をキャプチャする
ようになります。
fn main() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let mut borrows_mutably = || list.push(7);
borrows_mutably();
println!("After calling closure: {list:?}");
}
このコードはコンパイルされ、実行され、次のように出力されます。
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]
定義と borrows_mutably クロージャの呼び出しの間には、もはや
println! がないことに注意してください。borrows_mutably が定義される
とき、それは list への可変参照をキャプチャします。クロージャが
呼び出された後でそのクロージャを再び使うことはないので、可変借用は
そこで終わります。クロージャ定義とクロージャ呼び出しの間では、出力の
ための不変借用は許されません。可変借用があるときは、ほかの借用は
一切許されないからです。そこに println! を追加して、どのような
エラーメッセージが出るか試してみてください!
環境内で使う値について、クロージャ本体が厳密には所有権を必要としない
場合でも、クロージャに所有権を取らせたいなら、パラメータリストの前に
move キーワードを使えます。
この手法は主に、クロージャを新しいスレッドに渡してデータをムーブし、
その新しいスレッドが所有するようにしたいときに役立ちます。スレッドと、
なぜそれを使いたいのかについては、第 16 章で並行性を扱う際に詳しく
説明しますが、ここでは今のところ、move キーワードを必要とする
クロージャを使って新しいスレッドを生成する方法を簡単に見てみましょう。
リスト 13-6 は、リスト 13-4 を変更し、メインスレッドではなく新しい
スレッドでベクタを出力するようにしたものです。
use std::thread;
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
thread::spawn(move || println!("From thread: {list:?}"))
.join()
.unwrap();
}
新しいスレッドを生成し、そのスレッドに実行するクロージャを引数として
渡します。クロージャ本体はリストを出力します。リスト 13-4 では、
クロージャは list を不変参照でのみキャプチャしていました。これは、
list を出力するのに必要なアクセスがそれで十分だったからです。この例では、
クロージャ本体は依然として不変参照しか必要としないにもかかわらず、
クロージャ定義の先頭に move キーワードを置くことで、list を
クロージャの中へムーブするよう明示する必要があります。メインスレッドが
新しいスレッドに対して join を呼び出す前にさらに多くの操作を行うと、
新しいスレッドがメインスレッドの残りの処理より先に終了するかもしれませんし、
逆にメインスレッドが先に終了するかもしれません。もしメインスレッドが
list の所有権を保持したまま新しいスレッドより先に終了し、list を
ドロップした場合、スレッド内の不変参照は無効になります。したがって、
コンパイラは、新しいスレッドに渡されるクロージャの中へ list を
ムーブすることを要求します。そうすることで、その参照が有効になるからです。
move キーワードを削除したり、クロージャが定義された後でメインスレッド
内で list を使ったりして、どのようなコンパイラエラーが出るか試して
みてください!
クロージャからキャプチャした値をムーブする
クロージャが、定義された環境から参照をキャプチャしたり、値の所有権を キャプチャしたりすると(それによって、何がクロージャ 内に ムーブされるのか、あるいは何もムーブされないのかが影響を受けます)、 その後でクロージャが評価されるときにそれらの参照や値に何が起こるかは、 クロージャ本体のコードによって決まります(それによって、何がクロージャ 外に ムーブされるのか、あるいは何もムーブされないのかが影響を受けます)。
クロージャ本体は、次のいずれも行えます。キャプチャした値をクロージャの 外へムーブする、キャプチャした値を変更する、値をムーブも変更もしない、 あるいはそもそも環境から何もキャプチャしない、のいずれかです。
クロージャが環境から値をどのようにキャプチャし、それらをどう扱うかは、
そのクロージャがどのトレイトを実装するかに影響します。トレイトは、
関数や構造体がどの種類のクロージャを使えるかを指定する方法です。
クロージャは、クロージャ本体が値をどのように扱うかに応じて、これらの
Fn トレイトのうち 1 つ、2 つ、または 3 つすべてを、累積的に
自動実装します。
FnOnceは、1 回呼び出せるクロージャに適用されます。すべてのクロージャは 呼び出すことができるため、少なくともこのトレイトは実装します。キャプチャ した値を本体から外へムーブするクロージャは、1 回しか呼び出せないため、FnOnceのみを実装し、他のFnトレイトは実装しません。FnMutは、キャプチャした値を本体から外へムーブしないものの、キャプチャ した値を変更する可能性があるクロージャに適用されます。これらのクロージャは 複数回呼び出すことができます。Fnは、キャプチャした値を本体から外へムーブせず、かつキャプチャした値を 変更もしないクロージャに適用されます。また、環境から何もキャプチャしない クロージャにも適用されます。これらのクロージャは、環境を変更することなく 複数回呼び出すことができます。これは、たとえばクロージャを並行して複数回 呼び出すような場合に重要です。
それでは、リスト 13-1 で使った Option<T> の unwrap_or_else
メソッドの定義を見てみましょう。
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
T は、Option の Some バリアントに入っている値の型を表す
ジェネリック型であることを思い出してください。その型 T は、
unwrap_or_else 関数の戻り値の型でもあります。たとえば、
Option<String> に対して unwrap_or_else を呼び出すコードは、
String を受け取ることになります。
次に、unwrap_or_else 関数には追加のジェネリック型パラメータ F
があることに注目してください。型 F は f という名前の引数の型で、
これは unwrap_or_else を呼び出すときに渡すクロージャです。
ジェネリック型 F に指定されているトレイト境界は FnOnce() -> T
です。これは、F が 1 回呼び出し可能で、引数を取らず、T を返さ
なければならないことを意味します。トレイト境界に FnOnce を使うことで、
unwrap_or_else が f を 1 回より多く呼び出さないという制約を
表しています。unwrap_or_else の本体を見ると、Option が Some
であれば f は呼び出されません。Option が None であれば、f
は 1 回呼び出されます。すべてのクロージャは FnOnce を実装している
ため、unwrap_or_else は 3 種類すべてのクロージャを受け取ることが
でき、可能な限り柔軟です。
注: やりたいことが環境から値をキャプチャする必要がないのであれば、
Fnトレイトのいずれかを実装する何かが必要な箇所で、クロージャの代わりに 関数名を使うことができます。たとえば、Option<Vec<T>>の値に対しては、 値がNoneのときに新しい空のベクタを得るためにunwrap_or_else(Vec::new)を呼び出せます。コンパイラは、関数定義に 対して適用可能なFnトレイトを自動的に実装します。
次に、スライスに定義されている標準ライブラリメソッド sort_by_key を
見て、これが unwrap_or_else とどう異なるのか、そしてなぜ
sort_by_key がトレイト境界に FnOnce ではなく FnMut を使うのかを
確認しましょう。クロージャは、現在対象となっているスライス内の要素への
参照という形で 1 つの引数を受け取り、順序付け可能な型 K の値を返します。
この関数は、各要素の特定の属性でスライスをソートしたいときに便利です。
リスト 13-7 では、Rectangle インスタンスのリストがあり、それらを
width 属性の小さい順に並べるために sort_by_key を使います。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
list.sort_by_key(|r| r.width);
println!("{list:#?}");
}
このコードは次を出力します。
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/rectangles`
[
Rectangle {
width: 3,
height: 5,
},
Rectangle {
width: 7,
height: 12,
},
Rectangle {
width: 10,
height: 1,
},
]
sort_by_key が FnMut クロージャを受け取るように定義されている理由は、
そのクロージャを複数回呼び出すからです。スライス内の各要素について 1 回ずつ
呼び出します。クロージャ |r| r.width は、環境から何もキャプチャせず、何も変更せず、何も外へムーブしない
ため、トレイト境界の要件を満たします。
対照的に、リスト 13-8 は、環境から値をムーブするため、FnOnce
トレイトしか実装しないクロージャの例を示しています。コンパイラは、この
クロージャを sort_by_key で使うことを許してくれません。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut sort_operations = vec![];
let value = String::from("closure called");
list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{list:#?}");
}
これは、list をソートするときに sort_by_key がクロージャを何回
呼び出すかを数えようとする、不自然で回りくどい方法です(しかも動作しません)。
このコードは、クロージャの環境にある String である value を
sort_operations ベクタに push することで、その回数を数えようと
しています。クロージャは value をキャプチャし、その後 value
の所有権を sort_operations ベクタに移すことで、value をクロージャの
外へムーブします。このクロージャは 1 回しか呼び出せません。2 回目に
呼び出そうとしても、value はもはや環境内になく、再び
sort_operations に push できないからです。したがって、このクロージャは
FnOnce しか実装しません。このコードをコンパイルしようとすると、
クロージャは FnMut を実装しなければならないため、value を
クロージャの外へムーブできないという次のエラーが出ます。
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
--> src/main.rs:18:30
|
15 | let value = String::from("closure called");
| ----- ------------------------------ move occurs because `value` has type `String`, which does not implement the `Copy` trait
| |
| captured outer variable
16 |
17 | list.sort_by_key(|r| {
| --- captured by this `FnMut` closure
18 | sort_operations.push(value);
| ^^^^^ `value` is moved here
|
help: consider cloning the value if the performance cost is acceptable
|
18 | sort_operations.push(value.clone());
| ++++++++
For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error
このエラーは、value を環境から外へムーブしているクロージャ本体の行を
指しています。これを修正するには、クロージャ本体が環境から値をムーブしない
ように変更する必要があります。環境内にカウンタを保持し、クロージャ本体で
その値をインクリメントするほうが、クロージャが何回呼び出されたかを数える
より素直な方法です。リスト 13-9 のクロージャが sort_by_key で
動作するのは、num_sort_operations カウンタへの可変参照だけを
キャプチャしているためであり、したがって複数回呼び出すことができます。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut num_sort_operations = 0;
list.sort_by_key(|r| {
num_sort_operations += 1;
r.width
});
println!("{list:#?}, sorted in {num_sort_operations} operations");
}