高度な関数とクロージャ
この節では、関数とクロージャに関するいくつかの高度な機能、 具体的には関数ポインタやクロージャを返すことについて見ていきます。
関数ポインタ
関数にクロージャを渡す方法についてはすでに説明しましたが、通常の
関数を関数に渡すこともできます! この手法は、新しいクロージャを
定義する代わりに、すでに定義した関数を渡したいときに便利です。関数は
fn 型(小文字の f)に型強制されます。これは
Fn クロージャトレイトと混同しないでください。fn 型は
関数ポインタ と呼ばれます。関数ポインタを使って関数を渡すことで、
ほかの関数への引数として関数を利用できるようになります。
パラメータが関数ポインタであることを指定する構文は
クロージャの場合と似ています。リスト20-28では、引数に 1 を足す
add_one 関数を定義しています。do_twice 関数は 2 つの
パラメータを取ります。1 つは i32 パラメータを受け取り
i32 を返す任意の関数への関数ポインタ、もう 1 つは 1 個の i32
値です。do_twice 関数は f 関数を 2 回呼び出して arg
値を渡し、その 2 回の関数呼び出し結果を足し合わせます。main
関数は add_one と 5 を引数にして do_twice を呼び出します。
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {answer}");
}
このコードは The answer is: 12 を出力します。do_twice の
パラメータ f は、i32 型のパラメータを 1 つ受け取り
i32 を返す fn であると指定しています。そのため、
do_twice の本体の中で f を呼び出せます。main では、
add_one という関数名を do_twice の第 1 引数として渡せます。
クロージャとは異なり、fn はトレイトではなく型です。そのため、
Fn トレイトのいずれかをトレイト境界に持つジェネリック型パラメータを
宣言するのではなく、fn をパラメータ型として直接指定します。
関数ポインタは 3 つすべてのクロージャトレイト(Fn、FnMut、FnOnce)
を実装しているため、クロージャを期待する関数に対する引数として、
いつでも関数ポインタを渡せます。関数でもクロージャでも受け取れるように
するため、関数はジェネリック型といずれかのクロージャトレイトを使って
記述するのが最善です。
とはいえ、クロージャではなく fn のみを受け取りたい場面の 1 つは、
クロージャを持たない外部コードとやり取りするときです。C の関数は
関数を引数として受け取れますが、C にはクロージャがありません。
インラインで定義したクロージャと名前付き関数のどちらも使える例として、
標準ライブラリの Iterator トレイトが提供する map メソッドの利用を
見てみましょう。数値のベクタを文字列のベクタに変換するために map
メソッドを使うには、リスト20-29のようにクロージャを使えます。
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(|i| i.to_string()).collect();
}
あるいは、クロージャの代わりに関数名を map の引数にすることもできます。
リスト20-30は、それがどのようになるかを示しています。
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(ToString::to_string).collect();
}
利用可能な to_string という名前の関数が複数あるため、
「高度なトレイト」 節で説明した完全修飾構文を
使わなければならないことに注意してください。
ここでは、ToString トレイトで定義された to_string 関数を使っています。
標準ライブラリは、Display を実装するあらゆる型に対してこれを
実装しています。
第 6 章の 「enum の値」 節で説明したとおり、 私たちが定義する各 enum バリアントの名前は、初期化関数にもなります。 これらの初期化関数は、クロージャトレイトを実装する関数ポインタとして 使えます。つまり、リスト20-31にあるように、クロージャを受け取る メソッドの引数として初期化関数を指定できます。
fn main() {
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}
ここでは、map が呼び出される範囲内の各 u32 値に対して
Status::Value の初期化関数を使うことで、Status::Value
インスタンスを作成しています。このスタイルを好む人もいれば、
クロージャを使うほうを好む人もいます。どちらも同じコードに
コンパイルされるので、自分にとってより明確なスタイルを使ってください。
クロージャを返す
クロージャはトレイトによって表現されるため、クロージャを直接
返すことはできません。通常、トレイトを返したくなる多くの場合では、
その代わりに、そのトレイトを実装する具体型を関数の戻り値として使えます。
しかし、クロージャには返せる具体型がないため、通常はそれをクロージャに
対して行うことはできません。たとえば、クロージャがスコープから何らかの値を
キャプチャする場合、戻り値の型として関数ポインタ fn を使うことは
できません。
その代わりに、通常は第 10 章で学んだ impl Trait 構文を使います。
Fn、FnOnce、FnMut を使って、任意の関数型を返せます。
たとえば、リスト20-32のコードは問題なくコンパイルされます。
#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
}
しかし、第 13 章の 「クロージャ型の推論と注釈」 節で述べたように、各クロージャはそれぞれ固有の別個の型でもあります。 同じシグネチャで実装が異なる複数の関数を扱う必要があるなら、 それらにはトレイトオブジェクトを使う必要があります。 リスト20-33のようなコードを書くとどうなるか、考えてみましょう。
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
move |x| x + init
}
ここには returns_closure と returns_initialized_closure という 2 つの
関数があり、どちらも impl Fn(i32) -> i32 を返します。それらが返す
クロージャは、同じ型として記述されているにもかかわらず、互いに異なることに
注目してください。これをコンパイルしようとすると、Rust はこれが
うまくいかないことを教えてくれます。
$ cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0308]: mismatched types
--> src/main.rs:2:44
|
2 | let handlers = vec![returns_closure(), returns_initialized_closure(123)];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
9 | fn returns_closure() -> impl Fn(i32) -> i32 {
| ------------------- the expected opaque type
...
13 | fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
| ------------------- the found opaque type
|
= note: expected opaque type `impl Fn(i32) -> i32`
found opaque type `impl Fn(i32) -> i32`
= note: distinct uses of `impl Trait` result in different opaque types
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions-example` (bin "functions-example") due to 1 previous error
エラーメッセージは、impl Trait を返すたびに、Rust が一意の 不透明型 を
作成することを示しています。これは、Rust が私たちのために構築するものの詳細を
中から見ることができず、また Rust が生成する型を自分で書けるように推測することも
できない型です。したがって、これらの関数は同じトレイト Fn(i32) -> i32 を
実装するクロージャを返しますが、Rust がそれぞれに対して生成する不透明型は
別個のものです。(これは、第17章の
「Pin 型と Unpin トレイト」 で見たように、
異なる async ブロックが同じ出力型を持っていても、Rust がそれぞれに対して異なる
具象型を生成するのと似ています。)この問題に対する解決策は、これまでにも何度か
見てきました。リスト20-34のように、トレイトオブジェクトを使えます。
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| x + init)
}
このコードは問題なくコンパイルできます。トレイトオブジェクトの詳細については、 第18章の 「トレイトオブジェクトを使って共有された振る舞いを抽象化する」 の節を参照してください。
次に、マクロを見ていきましょう!