クロージャ:環境をキャプチャする匿名関数

Beginner

This tutorial is from open-source community. Access the source code

はじめに

クロージャ:環境をキャプチャする匿名関数へようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習することができます。

この実験では、Rust のクロージャを調べます。クロージャは、変数に保存したり、引数として渡したりできる匿名関数で、定義スコープから値をキャプチャすることでコードの再利用と動作のカスタマイズを可能にします。

クロージャ:環境をキャプチャする匿名関数

Rust のクロージャは、変数に保存したり、他の関数に引数として渡したりできる匿名関数です。クロージャを 1 つの場所で作成してから、他の場所でクロージャを呼び出して、異なるコンテキストで評価することができます。関数とは異なり、クロージャは定義されたスコープから値をキャプチャすることができます。これらのクロージャの機能がどのようにコードの再利用と動作のカスタマイズを可能にするかを示します。

クロージャを使った環境のキャプチャ

まずは、クロージャを使って、定義された環境から値をキャプチャして後で使う方法を調べましょう。ここにシナリオがあります。私たちの T シャツ会社は、定期的に、メーリングリストに登録された誰かに独占的な限定版の T シャツをプレゼントして、販促活動を行っています。メーリングリストに登録された人は、任意で、自分の好きな色をプロフィールに追加することができます。無料の T シャツの対象者が好きな色を設定している場合、その色の T シャツをもらいます。好きな色を指定していない場合、会社が現在最も多く持っている色の T シャツをもらいます。

これを実装する方法はたくさんあります。この例では、RedBlueの 2 つのバリアントを持つShirtColorという列挙型を使います(単純化のために利用可能な色の数を制限します)。会社の在庫を表すInventory構造体を定義し、shirtsというフィールドを持ち、これは現在の在庫の T シャツの色を表すVec<ShirtColor>を含んでいます。Inventoryに定義されたgiveawayメソッドは、無料の T シャツの当選者の任意の T シャツの色の好みを取得し、その人が受け取る T シャツの色を返します。この設定をリスト 13-1 に示します。

ファイル名:src/main.rs

#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(
        &self,
        user_preference: Option<ShirtColor>,
    ) -> ShirtColor {
      1 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 {
      2 shirts: vec![
            ShirtColor::Blue,
            ShirtColor::Red,
            ShirtColor::Blue,
        ],
    };

    let user_pref1 = Some(ShirtColor::Red);
  3 let giveaway1 = store.giveaway(user_pref1);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref1, giveaway1
    );

    let user_pref2 = None;
  4 let giveaway2 = store.giveaway(user_pref2);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref2, giveaway2
    );
}

リスト 13-1: T シャツ会社の無料配布の状況

mainで定義されたstoreには、この限定版の販促活動のために配布する 2 枚の青い T シャツと 1 枚の赤い T シャツが残っています[2]。赤い T シャツが好きなユーザー[3]と何の好みもないユーザー[4]に対して、giveawayメソッドを呼び出します。

再び、このコードはたくさんの方法で実装できます。ここでは、クロージャに焦点を当てるため、クロージャを使ったgiveawayメソッドの本体を除いて、既に学んだ概念にとどまります。giveawayメソッドでは、Option<ShirtColor>型のパラメータとしてユーザーの好みを取得し、user_preferenceに対してunwrap_or_elseメソッドを呼び出します[1]。Option<T>unwrap_or_elseメソッドは、標準ライブラリによって定義されています。1 つの引数を取ります。引数なしで、T型の値を返すクロージャ(この場合、Option<T>Someバリアントに格納されている型と同じ型、この場合はShirtColor)。Option<T>Someバリアントの場合、unwrap_or_elseSome内の値を返します。Option<T>Noneバリアントの場合、unwrap_or_elseはクロージャを呼び出し、クロージャが返す値を返します。

クロージャ式|| self.most_stocked()unwrap_or_elseの引数として指定します。これは、引数を持たないクロージャです(クロージャに引数がある場合は、2 つの垂直パイプの間に表示されます)。クロージャの本体はself.most_stocked()を呼び出します。ここでクロージャを定義しており、必要になったときにunwrap_or_elseの実装がクロージャを評価します。

このコードを実行すると、以下が表示されます。

The user with preference Some(Red) gets Red
The user with preference None gets Blue

ここで興味深い点は、現在のInventoryインスタンスでself.most_stocked()を呼び出すクロージャを渡したことです。標準ライブラリは、私たちが定義したInventoryShirtColor型、またはこのシナリオで使いたいロジックについて何も知る必要はありませんでした。クロージャは、selfInventoryインスタンスへの不変参照をキャプチャし、指定したコードとともにunwrap_or_elseメソッドに渡します。一方、関数はこのようにして環境をキャプチャすることはできません。

クロージャの型推論とアノテーション

関数とクロージャにはさらに違いがあります。クロージャは通常、fn関数のように、パラメータや戻り値の型を明示的に指定する必要はありません。関数では型アノテーションが必要ですが、それは型がユーザーに公開される明示的なインターフェイスの一部だからです。このインターフェイスを厳密に定義することは、関数が使用する値の型と返す値の型について誰もが合意することを確保するために重要です。一方、クロージャはこのような公開されたインターフェイスでは使用されません。クロージャは変数に保存され、名前を付けずに使用され、ライブラリのユーザーに公開されません。

クロージャは通常短く、狭いコンテキスト内でのみ関連性があり、任意のシナリオではなく、特定のコンテキスト内でのみ使用されます。これらの制限されたコンテキスト内では、コンパイラはパラメータの型と戻り値の型を推論することができます。これは、ほとんどの変数の型を推論することができるのと同じようになっています(コンパイラにクロージャの型アノテーションが必要な場合もまれにあります)。

変数と同様に、もっと冗長になる代わりに明示性と明確さを高めたい場合は、型アノテーションを追加することができます。クロージャの型をアノテーションするには、リスト 13-2 に示す定義のようになります。この例では、クロージャを定義して変数に保存していますが、リスト 13-1 のように、引数として渡す場所でクロージャを定義するのではなく、変数に保存しています。

ファイル名:src/main.rs

let expensive_closure = |num: u32| -> u32 {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    num
};

リスト 13-2: クロージャのパラメータと戻り値の型のオプショナルな型アノテーションの追加

型アノテーションを追加すると、クロージャの構文は関数の構文に似てきます。ここでは、比較のために、引数に 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  ;

1 行目は関数の定義を示し、2 行目は完全にアノテーションされたクロージャの定義を示しています。3 行目では、クロージャの定義から型アノテーションを削除しています。4 行目では、クロージャの本体が 1 つの式のみであるため省略可能なカッコを削除しています。これらはすべて有効な定義であり、呼び出されたときに同じ動作を行います。add_one_v3add_one_v4の行では、型が使用法から推論されるため、クロージャを評価してコンパイルする必要があります。これは、let v = Vec::new();が型アノテーションまたはVecに挿入するいくつかの型の値のいずれかを必要として、Rust が型を推論できるようにするのと同じです。

クロージャの定義では、コンパイラはそれぞれのパラメータと戻り値について 1 つの具体的な型を推論します。たとえば、リスト 13-3 は、引数として受け取った値をそのまま返す短いクロージャの定義を示しています。このクロージャは、この例の目的以外ではあまり役に立ちません。定義に型アノテーションを追加していないことに注意してください。型アノテーションがないため、最初はStringでクロージャを呼び出すことができます。その後、整数でexample_closureを呼び出そうとすると、エラーが発生します。

ファイル名:src/main.rs

let example_closure = |x| x;

let s = example_closure(String::from("hello"));
let n = example_closure(5);

リスト 13-3: 2 つの異なる型で型が推論されたクロージャを呼び出そうとする

コンパイラはこのエラーを表示します。

error[E0308]: mismatched types
 --> src/main.rs:5:29
  |
5 |     let n = example_closure(5);
  |                             ^- help: try using a conversion method:
`.to_string()`
  |                             |
  |                             expected struct `String`, found integer

最初にString値でexample_closureを呼び出すとき、コンパイラはxの型とクロージャの戻り値の型をStringと推論します。その型は次にexample_closureのクロージャに固定され、同じクロージャで異なる型を使用しようとするときに型エラーが発生します。

参照のキャプチャまたは所有権の移動

クロージャは、環境から値をキャプチャする方法が 3 つあり、これは関数がパラメータを受け取る 3 つの方法に直接対応しています。不変参照を借りる、可変参照を借りる、所有権を取得するです。クロージャは、関数の本体がキャプチャされた値をどのように使用するかに基づいて、これらのどれを使用するかを決定します。

リスト 13-4 では、listというベクトルへの不変参照をキャプチャするクロージャを定義しています。なぜなら、値を表示するだけなので、不変参照が必要だからです。

ファイル名:src/main.rs

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

  1 let only_borrows = || println!("From closure: {:?}", list);

    println!("Before calling closure: {:?}", list);
  2 only_borrows();
    println!("After calling closure: {:?}", list);
}

リスト 13-4: 不変参照をキャプチャするクロージャの定義と呼び出し

この例はまた、変数がクロージャの定義にバインドできること[1]、および後で変数名を関数名のように変数名と丸括弧を使ってクロージャを呼び出せること[2]を示しています。

listに同時に複数の不変参照を持てるため、クロージャ定義の前のコード、クロージャ定義の後でクロージャ呼び出しの前、およびクロージャ呼び出しの後でも、listにアクセスできます。このコードはコンパイルされ、実行され、以下のように表示されます。

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ベクトルに要素を追加するようにします。クロージャは現在、可変参照をキャプチャします。

ファイル名:src/main.rs

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

リスト 13-5: 可変参照をキャプチャするクロージャの定義と呼び出し

このコードはコンパイルされ、実行され、以下のように表示されます。

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 を変更して、メインスレッドではなく新しいスレッドでベクトルを表示するようにしたものです。

ファイル名:src/main.rs

use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

  1 thread::spawn(move || {
      2 println!("From thread: {:?}", list)
    }).join().unwrap();
}

リスト 13-6: moveを使ってスレッド用のクロージャにlistの所有権を強制的に譲渡する

新しいスレッドを生成し、スレッドに実行するクロージャを引数として渡します。クロージャの本体はリストを表示します。リスト 13-4 では、クロージャは表示に必要なlistへのアクセスが最も少ないため、不変参照を使ってlistをキャプチャしていました。この例では、クロージャの本体がまだ不変参照のみを必要としているにもかかわらず[2]、クロージャ定義の先頭にmoveキーワードを入れることで[1]、listをクロージャに移動させるように指定する必要があります。新しいスレッドはメインスレッドの残りの部分が終了する前に終了する場合もあれば、メインスレッドが先に終了する場合もあります。メインスレッドがlistの所有権を維持したまま、新しいスレッドよりも先に終了してlistを破棄すると、スレッド内の不変参照は無効になります。したがって、コンパイラは、参照が有効になるように、listを新しいスレッドに渡されるクロージャに移動させることを要求します。moveキーワードを削除したり、クロージャを定義した後にメインスレッドでlistを使用したりして、どのようなコンパイラエラーが表示されるか見てみてください!

クロージャからキャプチャされた値をクロージャ外に移動させることと Fn トレイト

クロージャが定義された環境から参照をキャプチャしたり、値の所有権を取得したりすると(これにより、クロージャに何かが移動するかどうかが影響されます)、クロージャの本体のコードは、後でクロージャが評価されたときに参照や値がどうなるかを定義します(これにより、クロージャから何かが移動するかどうかが影響されます)。

クロージャの本体は、次のいずれかを行うことができます。クロージャからキャプチャされた値をクロージャ外に移動させる、キャプチャされた値を変更する、値を移動も変更もしない、または最初から環境から何もキャプチャしない。

クロージャが環境から値をキャプチャして処理する方法は、クロージャが実装するトレイトに影響します。トレイトは、関数や構造体がどの種類のクロージャを使用できるかを指定する方法です。クロージャは、クロージャの本体が値をどのように処理するかに応じて、これらのFnトレイトのうちの 1 つ、2 つ、またはすべてを追加的に自動的に実装します。

  • FnOnceは、一度だけ呼び出せるクロージャに適用されます。すべてのクロージャは少なくともこのトレイトを実装します。なぜなら、すべてのクロージャは呼び出せるからです。クロージャの本体からキャプチャされた値を移動させるクロージャは、一度だけ呼び出せるため、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は、OptionSomeバリアントに含まれる値の型を表すジェネリック型です。その型Tはまた、unwrap_or_else関数の戻り値の型でもあります。たとえば、Option<String>に対してunwrap_or_elseを呼び出すコードは、Stringを取得します。

次に、unwrap_or_else関数には追加のジェネリック型パラメータFがあることに注意してください。F型は、fという名前のパラメータの型であり、これはunwrap_or_elseを呼び出すときに提供するクロージャです。

ジェネリック型Fに指定されたトレイト境界はFnOnce() -> Tであり、これはFが一度だけ呼び出せ、引数を取らず、Tを返すことができることを意味します。トレイト境界でFnOnceを使用することで、unwrap_or_elsefを最大で一度だけ呼び出すという制約が表現されています。unwrap_or_elseの本体では、OptionSomeの場合、fは呼び出されません。OptionNoneの場合、fは一度呼び出されます。すべてのクロージャはFnOnceを実装するため、unwrap_or_elseは最も多様なクロージャを受け付け、可能な限り柔軟です。

注:関数も 3 つのFnトレイトすべてを実装することができます。何を行うかが環境から値をキャプチャする必要がない場合、Fnトレイトの 1 つを実装するものが必要な場所で、クロージャの代わりに関数名を使用することができます。たとえば、Option<Vec<T>>値に対して、値がNoneの場合に新しい空のベクトルを取得するには、unwrap_or_else(Vec::new)を呼び出すことができます。

次に、スライスに定義された標準ライブラリのメソッドsort_by_keyを見てみましょう。これがunwrap_or_elseとどのように異なるか、およびなぜsort_by_keyがトレイト境界にFnMutを使用するのかを理解しましょう。クロージャは、考慮されているスライス内の現在の要素への参照の形式で 1 つの引数を取得し、比較可能なK型の値を返します。この関数は、各要素の特定の属性に基づいてスライスをソートしたい場合に便利です。リスト 13-7 では、Rectangleインスタンスのリストがあり、sort_by_keyを使用してそれらをwidth属性で昇順に並べ替えています。

ファイル名:src/main.rs

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

リスト 13-7: widthで四角形を並べ替えるためにsort_by_keyを使用する

このコードは次のように表示されます。

[
    Rectangle {
        width: 3,
        height: 5,
    },
    Rectangle {
        width: 7,
        height: 12,
    },
    Rectangle {
        width: 10,
        height: 1,
    },
]

sort_by_keyFnMutクロージャを取るように定義されている理由は、スライス内の各要素に対して一度ずつクロージャを呼び出すからです。クロージャ|r| r.widthは、環境から何もキャプチャせず、変更せず、移動しません。したがって、トレイト境界の要件を満たしています。

対照的に、リスト 13-8 は、FnOnceトレイトのみを実装するクロージャの例を示しています。なぜなら、値を環境から移動させるからです。コンパイラは、このクロージャをsort_by_keyと一緒に使用しようとしません。

ファイル名:src/main.rs

--snip--

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("by key called");

    list.sort_by_key(|r| {
        sort_operations.push(value);
        r.width
    });
    println!("{:#?}", list);
}

リスト 13-8: sort_by_keyFnOnceクロージャを使用しようとする

これは、listをソートするときにsort_by_keyが呼び出される回数を数えようとする、工夫された複雑な方法(機能しません)です。このコードは、クロージャの環境からのStringであるvaluesort_operationsベクトルにプッシュすることにより、このカウントを行おうとしています。クロージャはvalueをキャプチャし、その後、valueの所有権をsort_operationsベクトルに転送することにより、valueをクロージャから移動させます。このクロージャは一度だけ呼び出すことができます。2 回目に呼び出そうとすると機能しません。なぜなら、valueはもう環境に存在せず、再度sort_operationsにプッシュすることはできないからです!したがって、このクロージャはFnOnceのみを実装します。このコードをコンパイルしようとすると、valueがクロージャから移動できないというエラーが表示されます。なぜなら、クロージャはFnMutを実装しなければならないからです。

error[E0507]: cannot move out of `value`, a captured variable in an `FnMut`
closure
  --> src/main.rs:18:30
   |
15 |       let value = String::from("by key called");
   |           ----- captured outer variable
16 |
17 |       list.sort_by_key(|r| {
   |  ______________________-
18 | |         sort_operations.push(value);
   | |                              ^^^^^ move occurs because `value` has
type `String`, which does not implement the `Copy` trait
19 | |         r.width
20 | |     });
   | |_____- captured by this `FnMut` closure

エラーは、クロージャの本体のvalueを環境から移動させる行を指しています。これを修正するには、クロージャの本体を変更して、環境から値を移動させないようにする必要があります。環境にカウンターを保持し、クロージャの本体でその値をインクリメントすることは、sort_by_keyが呼び出される回数を数えるよりも単純な方法です。リスト 13-9 のクロージャはsort_by_keyと一緒に機能します。なぜなら、num_sort_operationsカウンターへの可変参照のみをキャプチャしているため、複数回呼び出すことができます。

ファイル名:src/main.rs

--snip--

fn main() {
    --snip--

    let mut num_sort_operations = 0;
    list.sort_by_key(|r| {
        num_sort_operations += 1;
        r.width
    });
    println!(
        "{:#?}, sorted in {num_sort_operations} operations",
        list
    );
}

リスト 13-9: sort_by_keyFnMutクロージャを使用することが許される

Fnトレイトは、クロージャを使用する関数や型を定義または使用する際に重要です。次のセクションでは、反復子について説明します。多くの反復子メソッドはクロージャ引数を取るため、続ける際にはこれらのクロージャの詳細を覚えておいてください!

まとめ

おめでとうございます!クロージャ:環境をキャプチャする匿名関数の実験を完了しました。LabEx でさらに実験を行って、スキルを向上させることができます。