コンテンツにスキップ

RustのFn・FnMut・FnOnce

クロージャのページでは、クロージャの書き方と、クロージャが変数をキャプチャしたり変更したりするしくみを学びました。このページでは、その次の疑問に答えます。クロージャを受け取る関数を自分で書くにはどうすればよいのでしょうか? そして、関数からクロージャを返すにはどうすればよいのでしょうか?

答えは、Fn・FnMut・FnOnce という3つの小さなトレイトです。図書館のルールにたとえてみましょう。本を読むだけの人がいます(Fn)。余白にメモを書き込む人もいます(FnMut)。そして、本を家に持ち帰って二度と返さない人もいます(FnOnce)。Rustはクロージャが何をしているかを見て、それに合ったラベルを貼ってくれます。最初はトレイトが難しそうに見えるかもしれませんが、心配はいりません。種類は3つだけで、どれにするかはコンパイラが選んでくれます。

そもそもクロージャにトレイトが必要な理由

Section titled “そもそもクロージャにトレイトが必要な理由”

クロージャには、コンパイラが作ったそのクロージャだけの型があります。その型の名前を書くことはできません。まったく同じに見える2つのクロージャでも、型は別物です。

fn main() {
let mut rules = vec![|price: u32| price * 2];
rules.push(|price: u32| price * 2);
}
error[E0308]: mismatched types
--> src/main.rs:3:16
|
3 | rules.push(|price: u32| price * 2);
| ---- ^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
|
= note: no two closures, even if identical, have the same type
= help: consider boxing your closure and/or using it as a trait object

コンパイラは「クロージャを期待していたのに、別のクロージャが来た」と言い、「同じに見えても、2つのクロージャが同じ型になることはない」と補足しています。

そのため、関数は「型Xのクロージャをください」とは書けません。代わりに「このように呼び出せる型なら何でもください」と書きます。これがトレイト境界です。ジェネリクスやトレイトのページで見たものと同じです。

fn price_after<F: Fn(u32) -> u32>(rule: F, price: u32) -> u32 {
rule(price)
}
fn main() {
let coupon = 15;
let half_price = |price: u32| price / 2;
let use_coupon = |price: u32| price - coupon;
println!("{}", price_after(half_price, 80));
println!("{}", price_after(use_coupon, 80));
}

実行結果:

40
65
▶ 実際に試してみよう

F: Fn(u32) -> u32 は「F は u32 を渡して呼び出すと u32 を返してくれる、何らかの型」と読みます。2つのクロージャは型が違いますが、どちらもこの条件に当てはまります。

コンパイラは、クロージャの本体がキャプチャした値に対して何をしているかを見ます。

  • Fn:値を読むだけ。何回でも呼び出せます。
  • FnMut:値を変更する。何回でも呼び出せますが、mut が必要です。
  • FnOnce:値を手放す(外へムーブする)。呼び出せるのは1回だけです。1回呼ぶと、その値はもう残っていないからです。
fn main() {
let shop = String::from("街角ベーカリー");
// Fn:`shop` を読むだけ
let greet = || println!("{shop}へようこそ!");
greet();
greet();
// FnMut:`orders` を変更する
let mut orders = 0;
let mut take_order = || {
orders += 1;
println!("{orders}件目の注文を受け付けました");
};
take_order();
take_order();
// FnOnce:`receipt` を呼び出し元に渡してしまう
let receipt = String::from("クロワッサン2個、コーヒー1杯");
let hand_over = || receipt;
let customer_copy = hand_over();
println!("お客さんの手元にあるもの: {customer_copy}");
}

実行結果:

街角ベーカリーへようこそ!
街角ベーカリーへようこそ!
1件目の注文を受け付けました
2件目の注文を受け付けました
お客さんの手元にあるもの: クロワッサン2個、コーヒー1杯
▶ 実際に試してみよう

hand_over が返すのは receipt のコピーではなく、receipt そのものです。レシートを渡してしまったら、クロージャにはもう渡せるものが残っていません。だから FnOnce なのです。

頭の中に、次のような図を描いておきましょう。3つの円が入れ子になっているところを想像してください。

  • いちばん外側の大きな円が FnOnce です。どんなクロージャも少なくとも1回は呼び出せるので、すべてのクロージャがここに入ります。
  • その内側にある小さめの円が FnMut です。何度でも呼び出せるクロージャです。
  • そしてど真ん中にあるのが Fn です。何も変更せずに何度でも呼び出せるクロージャです。

つまり、Fn のクロージャはすべて FnMut でもあり、FnOnce でもあります。これは関数を書くときに大切になります。FnOnce を求める引数は、どんなクロージャでも受け取れます。Fn を求める引数は、いちばん好みがうるさい引数です。

意外に思われがちなのが、move キーワードはトレイトを決めない、という点です。move || println!("{shop}") は shop の所有権を受け取りますが、読んでいるだけなので Fn のままです。決めるのは本体の中身だけです。

クロージャを受け取る関数を書く

Section titled “クロージャを受け取る関数を書く”

同じトレイト境界には3つの書き方があります。意味はどれも同じなので、読みやすいものを選んでください。

// 1. トレイト境界つきのジェネリックな型引数
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(x)
}
// 2. 引数の位置に `impl Fn`:短く書けて、名前もいらない
fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
// 3. `where` 句:トレイト境界が長くなったときにすっきり書ける
fn apply_to_all<F>(f: F, scores: &[i32]) -> Vec<i32>
where
F: Fn(i32) -> i32,
{
scores.iter().map(|&s| f(s)).collect()
}
fn main() {
let bonus = 10;
let add_bonus = |score: i32| score + bonus;
println!("{}", apply(add_bonus, 50));
println!("{}", apply_twice(add_bonus, 50));
println!("{:?}", apply_to_all(add_bonus, &[40, 65, 90]));
}

実行結果:

60
70
[50, 75, 100]
▶ 実際に試してみよう

add_bonus を3回も渡せるのはなぜだろう、と思ったかもしれません。キャプチャしたものを読むだけのクロージャは、手軽にコピーできます。そのため、Rustは渡すたびにコピーしているのです。

次は FnMut と FnOnce です。FnMut では、クロージャを呼び出すと中身が変わるので、引数に mut が必要です。FnOnce は1回呼び出したら、それでおしまいです。

fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) {
for task in tasks {
on_done(task);
}
}
fn on_shutdown<F: FnOnce()>(cleanup: F) {
println!("シャットダウンしています...");
cleanup();
}
fn main() {
let mut finished = Vec::new();
process_tasks(&["メール送信", "データベースのバックアップ"], |task| {
finished.push(task.to_string());
});
println!("完了したタスク: {:?}", finished);
on_shutdown(|| {
// このループが `finished` の所有権を受け取るので、クロージャは FnOnce になる
for task in finished {
println!("アーカイブしました: {task}");
}
});
}

実行結果:

完了したタスク: ["メール送信", "データベースのバックアップ"]
シャットダウンしています...
アーカイブしました: メール送信
アーカイブしました: データベースのバックアップ
▶ 実際に試してみよう

目安になるルールがあります。関数が受け入れられる範囲で、いちばんゆるいトレイトを求めましょう。クロージャを1回しか呼ばないなら FnOnce を求めれば、呼び出す側はどんなクロージャでも渡せます。標準ライブラリでも同じ考え方が使われています。Option::map は FnOnce を受け取り、Iterator::map は FnMut を受け取ります。

トレイト境界の選び方を間違えたとき

Section titled “トレイト境界の選び方を間違えたとき”

食い違いがあれば、コンパイラが必ず見つけてくれます。しかも、ここでのエラーメッセージはとても親切です。たとえば、関数は Fn を求めているのに、クロージャがカウンターを変更しているとしましょう。

fn run_report<F: Fn()>(f: F) {
f();
f();
}
fn main() {
let mut runs = 0;
run_report(|| {
runs += 1;
println!("レポートを実行しました({runs}回目)");
});
}
error[E0594]: cannot assign to `runs`, as it is a captured variable in a `Fn` closure
--> src/main.rs:9:9
|
1 | fn run_report<F: Fn()>(f: F) {
| - change this to accept `FnMut` instead of `Fn`
...
8 | run_report(|| {
| ---------- -- in this closure
| |
| expects `Fn` instead of `FnMut`
9 | runs += 1;
| ^^^^^^^^^ cannot assign

「Fn クロージャの中でキャプチャした変数 runs には代入できない」というエラーです。直し方まで教えてくれています。「Fn の代わりに FnMut を受け取るように変えてください」。引数に mut も付けましょう。この変更をすると、プログラムは レポートを実行しました(1回目) と レポートを実行しました(2回目) を表示します。

fn run_report<F: FnMut()>(mut f: F) {
f();
f();
}

もう1つのよくある間違いは、FnOnce のクロージャを2回呼び出すことです。

fn main() {
let report = String::from("第3四半期の売上");
let send = || {
let sent = report;
println!("{sent}を送信しています");
};
send();
send();
}
error[E0382]: use of moved value: `send`
--> src/main.rs:8:5
|
7 | send();
| ------ `send` moved due to this call
8 | send();
| ^^^^ value used here after move
|
note: closure cannot be invoked more than once because it moves the variable `report` out of its environment
--> src/main.rs:4:20
|
4 | let sent = report;
| ^^^^^^

note(補足)にはっきり書かれています。「このクロージャは変数 report を環境の外へムーブするので、2回以上呼び出せない」。1回目の呼び出しで report を手放してしまったので、2回目に渡せるものは何も残っていないのです。どうしても2回呼びたいなら、ムーブする代わりに借用しましょう(let sent = &report;)。そうすればクロージャは Fn になります。このようなクロージャを Fn を求める関数に渡した場合も、似たメッセージが出ます。error[E0507]: cannot move out of report, a captured variable in an Fn closure(Fn クロージャでキャプチャした report をムーブすることはできない)です。

関数はクロージャを作って返すこともできます。戻り値の型は impl Fn(i32) -> i32 と書きます。「何らかのクロージャの型。名前は知らなくていい」という意味です。

fn make_adder(amount: i32) -> impl Fn(i32) -> i32 {
move |x| x + amount
}
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
move |x| x * factor
}
fn main() {
let add_tip = make_adder(5);
let triple = make_multiplier(3);
println!("{}", add_tip(20));
println!("{}", triple(7));
println!("{}", add_tip(triple(10)));
}

実行結果:

25
21
35
▶ 実際に試してみよう

なぜ move が必要なのでしょうか? move がないと、クロージャは amount を借用するだけです。ところが amount は make_adder のもので、make_adder から戻った瞬間に消えてしまいます。クロージャは、何もない場所への参照を持つことになってしまうのです。Rustはこれを許しません。

error[E0373]: closure may outlive the current function, but it borrows `amount`, which is owned by the current function
--> src/main.rs:2:5
|
2 | |x| x + amount
| ^^^ ------ `amount` is borrowed here
| |
| may outlive borrowed value `amount`
|
help: to force the closure to take ownership of `amount` (and any other referenced variables), use the `move` keyword
|
2 | move |x| x + amount
| ++++

「クロージャが現在の関数より長生きするかもしれないのに、関数が所有する amount を借用している」というエラーで、help(ヒント)では「move キーワードを使って所有権を受け取らせましょう」と提案しています。move を付けると、クロージャは自分用の amount のコピーを持って出かけます。お弁当を他人の冷蔵庫に置いていくのではなく、自分で持っていくようなものです。

Box に入れたクロージャ:たくさんのクロージャを1つの型で

Section titled “Box に入れたクロージャ:たくさんのクロージャを1つの型で”

impl Fn が表すのは、隠された1つの型です。そのため、if の分岐からはあるクロージャを、else の分岐からは別のクロージャを返す、ということはできません。異なるクロージャを同じ Vec に入れることもできません。どちらの場合も、クロージャを1つずつ Box に入れましょう。Box<dyn Fn(f64) -> f64> は「この形をした任意のクロージャを、ヒープに置いたもの」という意味です。トレイトオブジェクトのページと同じ考え方です。

次の例は、名前つきの価格ルールのリストを順番に適用していくレジです。

fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> {
if express {
Box::new(move |total| total + fee * 2.0)
} else {
Box::new(move |total| total + fee)
}
}
fn main() {
let member_percent = 10.0;
let coupon = 5.0;
let rules: Vec<(&str, Box<dyn Fn(f64) -> f64>)> = vec![
("会員割引", Box::new(move |total: f64| total * (1.0 - member_percent / 100.0))),
("クーポン", Box::new(move |total: f64| total - coupon)),
("通常配送", shipping_rule(false, 3.5)),
];
let mut total = 80.0;
println!("最初: {total:.2}");
for (name, rule) in &rules {
total = rule(total);
println!("{name}の後: {total:.2}");
}
}

実行結果:

最初: 80.00
会員割引の後: 72.00
クーポンの後: 67.00
通常配送の後: 70.50
▶ 実際に試してみよう

Box に入れると、ヒープ領域のちょっとした確保が必要になり、呼び出しのたびにポインタを経由します。とはいえ、それが問題になることはほとんどありません。クロージャの型が1つなら impl Fn、たくさんあるなら Box<dyn Fn> を使いましょう。

構造体にクロージャを持たせることもできます。クロージャの型には名前が付けられないので、構造体をその型についてジェネリックにします。次の例は、クリックされると何かのコードを実行する小さなボタンです。

struct Button<F: Fn()> {
label: String,
on_click: F,
}
impl<F: Fn()> Button<F> {
fn click(&self) {
println!("[{}] がクリックされました", self.label);
(self.on_click)();
}
}
fn main() {
let user = String::from("Ada");
let save = Button {
label: String::from("保存"),
on_click: move || println!("{user}さんの下書きを保存しています"),
};
save.click();
save.click();
}

実行結果:

[保存] がクリックされました
Adaさんの下書きを保存しています
[保存] がクリックされました
Adaさんの下書きを保存しています
▶ 実際に試してみよう

(self.on_click)() の丸かっこに注目してください。これは「フィールドを取り出してから、それを呼び出す」という意味をRustに伝えています。クリックのたびにキャプチャしたものを変更したいなら、F: FnMut() にして、メソッドが &mut self を受け取るようにしましょう。

ふつうの関数も、値として渡すことができます。その型は、小文字の fn を使って fn(i32) -> i32 のように書きます。これは関数ポインタ(function pointer)と呼ばれ、コードの置き場所を指す単純なアドレスです。何もキャプチャしないクロージャも、関数ポインタに変換できます。

fn double(x: i32) -> i32 {
x * 2
}
fn square(x: i32) -> i32 {
x * x
}
fn apply_op(op: fn(i32) -> i32, x: i32) -> i32 {
op(x)
}
fn main() {
println!("{}", apply_op(double, 4));
println!("{}", apply_op(|x| x - 1, 4));
let steps: [fn(i32) -> i32; 3] = [double, square, |x| x + 1];
let mut value = 3;
for step in steps {
value = step(value);
}
println!("{value}");
}

実行結果:

8
3
37
▶ 実際に試してみよう

関数ポインタはすべて同じ型なので、Box に入れなくても配列にまとめられます。ただし注意点があります。何かをキャプチャしているクロージャは、関数ポインタにはなれません。コンパイラは closures can only be coerced to fn types if they do not capture any variables(変数を何もキャプチャしていないクロージャだけが fn 型に変換できる)と教えてくれます。逆方向にはうれしい話があります。ふつうの関数はどれも Fn・FnMut・FnOnce を実装しているので、さきほどの apply に double を渡すこともできるのです。多くのコードでクロージャのトレイトが使われるのはこのためです。関数とクロージャの両方を受け取れるからです。

FnMut の引数に mut を付け忘れる。 FnMut は呼び出すと中身が変わるので、その束縛は mut でなければなりません。

error[E0596]: cannot borrow `on_done` as mutable, as it is not declared as mutable
--> src/main.rs:3:9
|
3 | on_done(task);
| ^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
1 | fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) {
| +++

「on_done は mut として宣言されていないので、可変として借用できない」というエラーです。help の通りに mut を付ければ直ります。

-> impl Fn から異なるクロージャを返す。 見た目が似ていても、2つのクロージャは2つの型です。

fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 {
if express {
move |total| total + fee * 2.0
} else {
move |total| total + fee
}
}
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:5:9
|
2 | / if express {
3 | | move |total| total + fee * 2.0
| | ------------------------------
| | |
| | the expected closure
| | expected because of this
4 | | } else {
5 | | move |total| total + fee
| | ^^^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
6 | | }
| |_____- `if` and `else` have incompatible types
|
= note: expected closure `{closure@src/main.rs:3:9: 3:21}`
found closure `{closure@src/main.rs:5:9: 5:21}`
= note: no two closures, even if identical, have the same type
= help: consider boxing your closure and/or using it as a trait object
help: you could change the return type to be a boxed trait object
|
1 - fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 {
1 + fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> {
|
help: if you change the return type to expect trait objects, box the returned expressions
|
3 ~ Box::new(move |total| total + fee * 2.0)
4 | } else {
5 ~ Box::new(move |total| total + fee)
|

コンパイラの提案(Box に入れたトレイトオブジェクトを返す)に従いましょう。Box<dyn Fn(f64) -> f64> を返すようにして、それぞれのクロージャを Box::new(...) で包みます。

クロージャのフィールドをメソッドのように呼び出す。 self.on_click() と書くと、Rustは on_click という名前のメソッドを探してしまいます。

error[E0599]: no method named `on_click` found for reference `&Button<F>` in the current scope
--> src/main.rs:9:14
|
9 | self.on_click();
| ^^^^^^^^ field, not a method
|
help: to call the type parameter stored in `on_click`, surround the field access with parentheses
|
9 | (self.on_click)();
| + +

「それはメソッドではなくフィールドです」と言われています。help の通り、フィールドへのアクセスを丸かっこで囲みましょう。

関数 ticket_dispenser(start: u32) を書いてください。この関数はクロージャを返します。クロージャを呼び出すたびに、次の整理券番号が得られます。最初は start、次は start + 1、というように続きます。発券機を2台作り(パン屋は100から、薬局は1から)、それぞれが別々に数えていることを確かめましょう。ヒント: クロージャは自分のカウンターを変更するので、戻り値の型は impl FnMut() -> u32 です。move も必要になります。

解答を見る
fn ticket_dispenser(start: u32) -> impl FnMut() -> u32 {
let mut next = start;
move || {
let ticket = next;
next += 1;
ticket
}
}
fn main() {
let mut bakery = ticket_dispenser(100);
let mut pharmacy = ticket_dispenser(1);
println!("パン屋: {}", bakery());
println!("パン屋: {}", bakery());
println!("薬局: {}", pharmacy());
println!("パン屋: {}", bakery());
}

実行結果:

パン屋: 100
パン屋: 101
薬局: 1
パン屋: 102
▶ 実際に試してみよう

それぞれの発券機が自分用の next をクロージャの中にムーブしているので、カウンターを共有することはありません。変数 bakery と pharmacy は FnMut なので、mut が必要です。

2. リトライ(再試行)ヘルパー

Section titled “2. リトライ(再試行)ヘルパー”

retry(max_attempts, task) を書いてください。この関数は、task が Ok を返すか、max_attempts 回試すまで、task を呼び出し続けます。失敗するたびに1行表示してください。2回は "サーバーが混雑しています" で失敗し、3回目で成功するクロージャを使って試しましょう。ヒント: テスト用のクロージャは自分の呼び出し回数を数えるので、トレイト境界は FnMut() -> Result<String, String> です。引数には mut が必要です。

解答を見る
fn retry<F>(max_attempts: u32, mut task: F) -> Result<String, String>
where
F: FnMut() -> Result<String, String>,
{
for attempt in 1..=max_attempts {
match task() {
Ok(value) => return Ok(value),
Err(e) => println!("{attempt}回目の試行に失敗: {e}"),
}
}
Err(String::from("あきらめました"))
}
fn main() {
let mut calls = 0;
let result = retry(5, || {
calls += 1;
if calls < 3 {
Err(String::from("サーバーが混雑しています"))
} else {
Ok(String::from("データをダウンロードしました"))
}
});
println!("{:?}(呼び出し回数: {calls}回)", result);
}

実行結果:

1回目の試行に失敗: サーバーが混雑しています
2回目の試行に失敗: サーバーが混雑しています
Ok("データをダウンロードしました")(呼び出し回数: 3回)
▶ 実際に試してみよう

retry はタスクを2回以上呼び出すので、FnOnce では足りません。また、タスクは calls を変更するので、Fn も使えません。FnMut がぴったりです。retry から戻った時点でクロージャの出番は終わっているので、main はまた calls を読めるようになります。

  • クロージャにはそれぞれ固有の型があるので、関数は F: Fn(i32) -> i32 のようなトレイト境界を使ってクロージャを受け取ります。
  • Fn はキャプチャしたものを読むだけ、FnMut は変更し、FnOnce は外へムーブするので1回しか呼べません。決めるのは本体の中身で、move ではありません。
  • Fn はすべて FnMut でもあり FnOnce でもあります。関数が必要とする範囲で、いちばんゆるいトレイトを求めましょう。
  • トレイト境界は <F: Fn(..)>、impl Fn(..)、または where 句で書けます。FnMut の引数には mut が必要です。
  • 1つのクロージャを返すなら -> impl Fn(..)(たいてい move と一緒に)を使います。分岐ごとに異なるクロージャを返すときや Vec に入れるときは Box<dyn Fn(..)> を使います。
  • fn(i32) -> i32 は関数ポインタです。ふつうの関数と、何もキャプチャしないクロージャがこの型に当てはまります。

理解度チェック

1. クロージャがキャプチャした変数に total += price をしています。このクロージャが実装する、いちばん厳しいトレイトはどれですか?

2. あなたの関数は、引数のクロージャをちょうど1回だけ呼び出します。いちばん多くのクロージャを受け取れるトレイト境界はどれですか?

3. make_adder(amount: i32) -> impl Fn(i32) -> i32 で move |x| x + amount が必要なのはなぜですか?

4. いくつかの異なる割引用クロージャを入れた Vec を作りたいです。要素の型は何にすればよいですか?

スコア: 0 / 4