Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

高度な関数とクロージャ

この節では、関数とクロージャに関するいくつかの高度な機能、 具体的には関数ポインタやクロージャを返すことについて見ていきます。

関数ポインタ

関数にクロージャを渡す方法についてはすでに説明しましたが、通常の 関数を関数に渡すこともできます! この手法は、新しいクロージャを 定義する代わりに、すでに定義した関数を渡したいときに便利です。関数は 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_one5 を引数にして 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 つすべてのクロージャトレイト(FnFnMutFnOnce) を実装しているため、クロージャを期待する関数に対する引数として、 いつでも関数ポインタを渡せます。関数でもクロージャでも受け取れるように するため、関数はジェネリック型といずれかのクロージャトレイトを使って 記述するのが最善です。

とはいえ、クロージャではなく 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 構文を使います。 FnFnOnceFnMut を使って、任意の関数型を返せます。 たとえば、リスト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_closurereturns_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章の 「トレイトオブジェクトを使って共有された振る舞いを抽象化する」 の節を参照してください。

次に、マクロを見ていきましょう!