高度な関数とクロージャ

最後に関数とクロージャに関連する高度な機能の一部を探求し、これには関数ポインタとクロージャの返却が含まれます。

関数ポインタ

クロージャを関数に渡す方法について語りました; 普通の関数を関数に渡すこともできるのです! 新しいクロージャを定義するのではなく、既に定義した関数を渡したい時にこのテクニックは有用です。 これを関数ポインタで行うと、関数を引数として他の関数に渡して使用できます。関数は、型fn(小文字のfです)に型強制されます。 Fnクロージャトレイトと混同すべきではありません。fn型は、関数ポインタと呼ばれます。 引数が関数ポインタであると指定する記法は、クロージャのものと似ています。リスト19-35のように。

ファイル名: src/main.rs

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);
}

リスト19-35: fn型を使用して引数として関数ポインタを受け入れる

このコードは、The answer is: 12と出力します。do_twiceの引数fは、型i32の1つの引数を取り、 i32を返すfnと指定しています。それから、do_twiceの本体でfを呼び出すことができます。 mainでは、関数名のadd_oneを最初の引数としてdo_twiceに渡せます。

クロージャと異なり、fnはトレイトではなく型なので、トレイト境界としてFnトレイトの1つでジェネリックな型引数を宣言するのではなく、 直接fnを引数の型として指定します。

関数ポインタは、クロージャトレイト3つ全て(FnFnMutFnOnce)を実装するので、常に関数ポインタを引数として、 クロージャを期待する関数に渡すことができます。関数が関数とクロージャどちらも受け入れられるように、 ジェネリックな型とクロージャトレイトの1つを使用して関数を書くのが最善です。

クロージャではなくfnだけを受け入れたくなる箇所の一例は、クロージャのない外部コードとのインターフェイスです: C関数は引数として関数を受け入れられますが、Cにはクロージャがありません。

インラインでクロージャが定義されるか、名前付きの関数を使用できるであろう箇所の例として、mapの使用に目を向けましょう。 map関数を使用して数字のベクタを文字列のベクタに変換するには、このようにクロージャを使用できるでしょう:


# #![allow(unused_variables)]
#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に引数として関数に名前を付けられるでしょう:


# #![allow(unused_variables)]
#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を実装するあらゆる型に実装しています。

このスタイルを好む方もいますし、クロージャを使うのを好む方もいます。どちらも結果的に同じコードにコンパイルされるので、 どちらでも、自分にとって明確な方を使用してください。

クロージャを返却する

クロージャはトレイトによって表現されます。つまり、クロージャを直接は返却できないのです。 トレイトを返却したい可能性のあるほとんどの場合、代わりにトレイトを実装する具体的な型を関数の戻り値として使用できます。 ですが、クロージャではそれはできません。返却可能な具体的な型がないからです; 例えば、 関数ポインタのfnを戻り値の型として使うことは許容されていません。

以下のコードは、クロージャを直接返そうとしていますが、コンパイルできません:

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
 -->
  |
1 | fn returns_closure() -> Fn(i32) -> i32 {
  |                         ^^^^^^^^^^^^^^ `std::ops::Fn(i32) -> i32 + 'static`
  does not have a constant size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for
  `std::ops::Fn(i32) -> i32 + 'static`
  = note: the return type of a function must have a statically known size

エラーは、再度Sizedトレイトを参照しています!コンパイラには、クロージャを格納するのに必要なスペースがどれくらいかわからないのです。 この問題の解決策は先ほど見かけました。トレイトオブジェクトを使えます:


# #![allow(unused_variables)]
#fn main() {
fn returns_closure() -> Box<Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}
#}

このコードは、問題なくコンパイルできます。トレイトオブジェクトについて詳しくは、 第17章の「トレイトオブジェクトで異なる型の値を許容する」節を参照してください。

まとめ

ふう!もう道具箱に頻繁には使用しないRustの機能の一部がありますが、非常に限定された状況で利用可能だと知るでしょう。 エラーメッセージや他の方のコードで遭遇した際に、これらの概念や記法を認識できるように、 複雑な話題をいくつか紹介しました。この章は、解決策へ導く参考文献としてご活用ください。

次は、本を通して議論してきた全てを実践に配備し、もう1つプロジェクトを(こな)します!