Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
42
Help us understand the problem. What is going on with this article?
@shortheron

Rustのクロージャtraitについて調べた(FnOnce, FnMut, Fn)

概要

Rust勉強中です。
クロージャを扱うときに出てくる三つのtrait (FnOnce, FnMut, Fn) について、主に以下の観点で調べたので、備忘録的にまとめます。

  • クロージャを定義したとき、どのtraitが実装されるのか
  • クロージャを扱うときにどのtraitを使えばいいのか

誤りなどありましたらご指摘いただければと思います。
なお、参考文献は末尾にまとめました。

以下の内容は、v1.23.0 (2018/1/12時点の最新のstable)に基づきます。

追記(2021/02/14): 最新版(v1.50.0)に基づいた内容に変更、文章の修正、参考文献の更新

前提: クロージャとFnOnce, FnMut, Fnについて

Rustでクロージャを定義すると、キャプチャした変数をメンバとして持つ匿名構造体が、コンパイル時に内部的に作られます。12

この内部的に作られる型はコンパイル時にしか分からないため、「クロージャを受け取る関数」を作ろうと思っても、クロージャの型をコード上で直接指定することはできません。

代わりに、FnOnce, FnMut, Fn という三つのtraitを使うことで、クロージャ(に限らず任意の関数など)を受け取る関数を作ることができます。
クロージャ定義時に自動生成される型は、この三つのtraitのうち一つ以上を実装しています。

具体的には、これらのtraitのうちいずれかをジェネリック境界としたジェネリック関数を作ることで、クロージャを受け取る関数を作れます:

クロージャを受け取る関数の例
// 「i32を受け取りi32を返すクロージャ」を受け取る関数
fn call_with_42<F>(f: F) where F: FnOnce(i32) -> i32 {
    println!("f(42) = {}", f(42));
}

call_with_42(|x| 2 * x);

何故FnOnce, FnMut, Fnの三つのtraitがあり、それぞれどう使い分けられるのかがこの記事の主題になります。
それを見ていく前に、それぞれのtraitの定義を確認しておきたいと思います。345

pub trait FnOnce<Args> {
    type Output;
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

pub trait FnMut<Args>: FnOnce<Args> {
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

pub trait Fn<Args>: FnMut<Args> {
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

それぞれの違いは、self&mut self&selfのいずれを受け取るかです。

また、もう一つ重要なのは、FnFnMutを、FnMutFnOnceを継承していることです。次の項で詳しく見ますが、これらはクロージャ間の包含関係を表しています。

  • FnMut を実装しているクロージャは、FnOnce も実装している
  • Fn を実装しているクロージャは、FnMut, FnOnce も実装している

なお、FnOnce trait を介した呼び出しはselfの所有権をとるので、FnOnceは(その名の通り)一回しか呼び出せないことにも注意が必要です。

どのtraitがいつ実装されるか

本題です。
上述の通り、クロージャが実装しうるtraitは三つあるので、「じゃあ、どう使い分けられているのか?」という疑問が湧きます。

結論から言えば、各traitは、それぞれクロージャが特定の条件を満たしているかどうかに応じて、自動的に実装されるかどうかが変わります:

  • FnOnceは、全てのクロージャが実装している
  • FnMutは、キャプチャした変数をmoveしない全てのクロージャが実装している
  • Fnは、キャプチャした変数をmoveせず、書き換えもしない全てのクロージャが実装している

逆から言えば、以下のようになります。

  • クロージャがキャプチャした変数をmoveしているなら、FnOnceだけを実装している(FnMut, Fnは実装されない)
  • クロージャがキャプチャした変数をmoveしていないが、書き換えているならば、FnMut, FnOnceだけを実装している(Fnは実装されない)
  • クロージャがキャプチャした変数をmoveしておらず、書き換えてもいないならば、Fn, FnMut, FnOnceを全て実装している

考え方

上記は暗記する必要はなく、以下のことを考慮すれば自然に導けます。

  • クロージャを定義すると、内部で匿名の構造体が作られること
  • キャプチャした変数は、構造体のメンバとなること

たとえば、以下のようにクロージャを定義したとします:

struct Data(i32)

let x = Data(1);
let c = || {
    // ここでxを使ってなにかやる
    ...
};

このとき、内部的に以下のような型が作られます。(イメージです)

struct Closure {
    // ここでは例として `&Data` 型で持っているが、ケースによっては `&mut Data` や `Data` になる
    x: &Data,
}

そして、以下のそれぞれのimplが可能なら作られます。(イメージです)

impl FnOnce() for Closure {
    fn call_once(self) {
        ...
    }
}

impl FnMut() for Closure {
    fn call_mut(&mut self) {
        ...
    }
}

impl Fn() for Closure {
    fn call(&self) {
        ...
    }
}

各「...」のところには、クロージャの定義時に書いた処理の中身が入ります(この際、xself.xに置き換えて考えます)。

ここで、「可能なら」と言ったのは、処理の中身によってはimplできないことがあるからです。

  • 処理の中で x を書き換えていたら、 selfを受け取るFnOnce&mut selfを受け取るFnMutのimplは成立するが、&selfを受け取るFnのimplは成立しない
  • 処理の中で x の所有権を手放していたら、selfの所有権をとらないFnMutFnの定義は成立しない

このことから、処理の中身に応じて自然に「三つのtraitのどれが実装されるか」が決まると考えられます。

以下、具体的な例を見ていきます。

例1: キャプチャした変数をmoveしていればFnOnceのみ実装

キャプチャした変数の所有権をどこかにmoveしている場合、FnOnceのみが実装されます。

例1
// FnOnceのみ渡せる関数
fn call_fn_once<F>(f: F) where F: FnOnce() {
    f();
}

// FnMutのみ渡せる関数
fn call_fn_mut<F>(mut f: F) where F: FnMut() {
    f();
}

// Fnのみ渡せる関数
fn call_fn<F>(f: F) where F: Fn() {
    f();
}

struct Data(i32);

// This function takes ownership of x.
fn consume(x: Data) {
    println!("{}", x.0);
}

// ok
let x = Data(0);
call_fn_once(|| {
    consume(x);
});

// error: cannot move out of `x`, a captured variable in an `FnMut` closure
// let x = Data(0);
// call_fn_mut(|| {
//     consume(x);
// });

// error: cannot move out of `x`, a captured variable in an `Fn` closure
// let x = Data(0);
// call_fn(|| {
//    consume(x);
// });

上の考え方に照らせば、このときにFnMutが実装されないのは、以下のような定義が成立しえないためだと考えられます:

fn call_mut(&mut self) {
    // NG: mut参照からconsumeすることはできない
    consume(self.x);
}

Fnも同様です。

なお、上述のように、FnOnceしか実装していないクロージャは、一回しか呼び出せません
キャプチャした変数の所有権を初回呼出時に手放してしまうことを考えれば、納得できます。

例2: キャプチャした変数をmoveせず書き換えていればFnOnceFnMutを実装

キャプチャした変数を書き換えているが所有権のmoveはしていない場合、FnOnceFnMutが実装されます。

例2
// FnOnceのみ渡せる関数
fn call_fn_once<F>(f: F) where F: FnOnce() {
    f();
}

// FnMutのみ渡せる関数
fn call_fn_mut<F>(mut f: F) where F: FnMut() {
    f();
}

// Fnのみ渡せる関数
fn call_fn<F>(f: F) where F: Fn() {
    f();
}

struct Data(i32);

// ok
let mut x = Data(0);
call_fn_once(|| {
    x.0 = 1;
});

// ok
let mut x = Data(0);
call_fn_mut(|| {
    x.0 = 1;
});

// error: cannot assign to `x.0`, as `Fn` closures cannot mutate their captured variables
// let mut x = Data(0);
// call_fn(|| {
//    x.0 = 1;
// });

例1と同様に、Fnが実装されないのは、以下のような定義が成立しえないためと考えられます:

fn call(&self) {
    // NG: mutでない参照を通じて書き換えることはできない
    self.x = 1;
}

例3: キャプチャした変数をmoveも書き換えもしていなければFnOnceFnMutFnを全て実装

上記のいずれにも当てはまらない場合、FnOnceFnMutFnが全て実装されます。

例3
// FnOnceのみ渡せる関数
fn call_fn_once<F>(f: F) where F: FnOnce() {
    f();
}

// FnMutのみ渡せる関数
fn call_fn_mut<F>(mut f: F) where F: FnMut() {
    f();
}

// Fnのみ渡せる関数
fn call_fn<F>(f: F) where F: Fn() {
    f();
}

struct Data(i32);

// ok
let x = Data(0);
call_fn_once(|| {
    println!("{}", x.0);
});

// ok
let x = Data(0);
call_fn_mut(|| {
    println!("{}", x.0);
});

// ok
let x = Data(0);
call_fn(|| {
    println!("{}", x.0);
});

補足1: キャプチャした変数がCopyを実装している場合

例1の補足です。
キャプチャした変数がCopyトレイトを実装している場合は、所有権の移動は起こりません。
そのため、Copyトレイトを実装している型の変数(i32など)をキャプチャしてクロージャ内で他の関数に渡しても、FnOnceだけが実装されることはなく、常にFnMutもしくはFnまで実装されます。

例4(例1でDataをCopy可能にした場合)
// FnOnceのみ渡せる関数
fn call_fn_once<F>(f: F) where F: FnOnce() {
    f();
}

// FnMutのみ渡せる関数
fn call_fn_mut<F>(mut f: F) where F: FnMut() {
    f();
}

// Fnのみ渡せる関数
fn call_fn<F>(f: F) where F: Fn() {
    f();
}

// Copyを実装
#[derive(Clone, Copy)]
struct CopyableData(i32);

// DataがCopyを実装しているので、moveではなくcopyされる
fn not_consume(x: CopyableData) {
    println!("{}", x.0);
}

// ok
let x = CopyableData(0);
call_fn_once(|| {
    not_consume(x);
});

// ok
let x = CopyableData(0);
call_fn_mut(|| {
    not_consume(x);
});

// ok
let x = CopyableData(0);
call_fn(|| {
    not_consume(x);
});

補足2: moveクロージャとの関係

クロージャの定義時にmoveキーワードを使うことで、キャプチャした変数の所有権のクロージャへの移動を強制します。

moveクロージャの例
struct Data(i32);

let x = Data(0);
let c = move || {
    println!("in closure: {}", x.0);
};

// xはクロージャにmoveされているので、ここでは使えない
// error: borrow of moved value: `x`
//println!("in main: {}", x.0);

c();

うえで、クロージャはキャプチャした変数をメンバに持つ構造体として考えられると書きましたが、moveキーワードの有無によって、このメンバが参照になるか実体になるかが変わる、と解釈できます:6

// moveなしで、xの所有権も必要としない場合
struct Closure {
    x: &Data,
}

// moveあり、もしくはxの所有権を必要とする場合
struct Closure {
    x: Data,
}

では、moveを付けることで、「クロージャがFnOnceFnMutFnのどれを実装するか」に影響するでしょうか。
結論としては、moveを付けてもクロージャがFnOnceFnMutFnのどれを実装するかには影響しません
すなわち、上の例1~例4は全て、クロージャにmoveを付けてもそのまま成り立ちます。

実際、うえのようなクロージャの構造体表現を考えてみると、クロージャがキャプチャした変数の所有権を保持していたとしても、&mut selfを通してキャプチャした変数を書き換えられることなどには影響しないことから分かります。

moveクロージャかどうかと、FnOnceFnMutFnのいずれを実装しているかという問題は、それぞれ別の段階での所有権の移動に紐づいています:

  • moveクロージャかどうか
    • 環境⇒クロージャへの所有権の移動に関係
  • FnOnceFnMutFnのいずれを実装しているか
    • クロージャ⇒外部の関数等への所有権の移動に関係

"The Book" には以下のように書かれています:7

Note: move closures may still implement Fn or FnMut, even though they capture variables by move. This is because the traits implemented by a closure type are determined by what the closure does with captured values, not how it captures them. The move keyword only specifies the latter.

クロージャを受け取るときにどのtraitを使うべきか

実際に「クロージャを受け取る関数」を書くときに、どのtraitで受け取るべきかを考えてみます。

ここまで説明したように、クロージャの間には以下の包含関係があります。

(Fnを実装したクロージャの集合) ⊂ (FnMutを実装したクロージャの集合) ⊂ (FnOnceを実装したクロージャの集合) 

fn.png

可能な限りこのベン図のより外側にあるtraitを使ったほうが、より多くのクロージャを受け取れるということになります。
逆に、もしクロージャを受け取るときにFnで受け取るようにすると、一部のクロージャが受け取れないことを意味します。
なので、可能な限りFnよりFnMut、またFnMutよりFnOnceでクロージャを受け取るべき、と考えられます。

具体的には、以下のように決めればよいと思います。

  • 渡されたクロージャを一回しか呼び出さないならば、FnOnceで受け取る
  • 渡されたクロージャを複数回呼び出す可能性があるなら、FnMutで受け取る
    • 標準ライブラリでは、Iteratormapなどのメソッドが該当
  • クロージャのimmutable性を要求したい場合はFnで受け取る
    • こちらは少し探してみても標準ライブラリでは例が見つかりませんでした
    • たとえば、複数のスレッドから一つのクロージャを呼び出したいときなどには該当するかと思われます

クロージャを返すときにどのtraitを使うべきか

追記(2021/02/14): implを使った内容に書き換え

同様に、クロージャを返す時にどのtraitを使うべきか考えます。
まず前提として、クロージャを関数から返す方法について確認しておきます。

クロージャの具体的な型はコーディング時には分からないので、返り値型を直接書き下すことができません:

fn make_counter(init: i32, inc: i32) -> /* ここに何を書くか */ {
    let mut x = init;
    move || {
        x += inc;
        x - inc
    }
}

これは、Rustの1.26で導入された impl Traitという機能を使って書くことができます:8

例(impl-Trait)
fn make_counter(init: i32, inc: i32) -> impl FnMut() -> i32 {
    let mut x = init;
    move || {
        x += inc;
        x - inc
    }
}

let mut c = make_counter(5, 2);
println!("{}", c()); // 5
println!("{}", c()); // 7
println!("{}", c()); // 9

impl FnMut() -> i32 は、「FnMut() -> i32を実装した何らかの型」を意味します。
クロージャの型を直接指定することができないので、「FnMutを実装している何か」であることだけ明示する、という感じです。

また、Boxでラップしてトレイトオブジェクトとして返却する方法もあります(Rustの1.26より前のバージョンではこの方法しかありませんでした):

例(dyn-Trait)
// 最新版では `Box<dyn FnMut() -> i32>` とdynを付けないと警告
fn make_counter(init: i32, inc: i32) -> Box<dyn FnMut() -> i32> {
    let mut x = init;
    Box::new(move || {
        x += inc;
        x - inc
    })
}

let mut c = make_counter(5, 2);
println!("{}", c()); // 5
println!("{}", c()); // 7
println!("{}", c()); // 9

Boxを使った方法は、返却された関数の呼び出し時に動的ディスパッチが伴うので、可能なときは前者のimplを使ったほうが望ましいです(ただしimplを使った方法がとれない場合もあります)

さて、(implを使うにしろdynを使うにしろ)FnOnceFnMutFnのいずれかのtraitを指定してやる必要があります。

例えばうえの例で返却しているクロージャはFnOnceFnMutを実装していますが、これをFnOnceとして返却してしまうと、呼び出し側は「FnOnceを実装した何か」ということしか分からないのでこのクロージャを一回しか呼び出すことができません。

より一般に、引数で受け取るときとは逆に、可能な限りFnOnceよりもFnMut、またFnMutよりもFnとして返すべきであると言えます。
FnFnMutでもあり、FnMutFnOnceでもある」わけですから、こうすることで、呼び出し側でより広い範囲で返却されたクロージャを使うことができます。

参考文献

以下のページを参考にしました。

42
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
42
Help us understand the problem. What is going on with this article?