Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

概要

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

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

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

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

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

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

この匿名の型は、自動的に FnOnce, FnMut, Fn のいずれか(もしくは全て)のtraitを実装します。
そこで、これらをジェネリック境界としてジェネリック関数を作ることで、クロージャを受け取る関数を作れます:

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

call_with_one(|x| 2 * x);

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

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は実装されない)
    • そうでないならば、Fn, FnMut, FnOnceを全て実装している

考え方

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

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

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

struct Data(i32)

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

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

struct Closure {
    x: &Data,
}

(ここではxの参照をメンバに持っていますが、xの所有権が必要なときは、x: Dataといった実体をメンバに持つと考えられます。また、後述のmoveキーワードを付けたときも、実体を持つと考えられます)

そして、以下のそれぞれの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に置き換えます)。

ここで、「可能なら」と言ったのは、処理の中身によっては定義できないことがあるからです。
たとえば、処理の中でxを書き換えているならば、&selfを受け取るFnの定義は成立しません。
同様に、処理の中でxの所有権を手放しているならば、selfの所有権をとらないFnMutFnの定義は成立しません。

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

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

例1: FnOnceのみ実装

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

例1
fn call_fn<F>(f: F) where F: Fn() {
    f();
}

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

fn call_fn_once<F>(f: F) where F: FnOnce() {
    f();
}

struct Data(i32);

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

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

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

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

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

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

Fnも同様です。

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

例2: FnOnceFnMutのみ実装

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

例2
fn call_fn<F>(f: F) where F: Fn() {
    f();
}

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

fn call_fn_once<F>(f: F) where F: FnOnce() {
    f();
}

struct Data(i32);

// error: cannot assign to data in a captured outer variable in an `Fn` closure
// let mut x = Data(0);
// call_fn(|| {
//    x.0 = 1;
// });

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

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

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

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

例3: FnOnceFnMutFnを全て実装

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

例3
fn call_fn<F>(f: F) where F: Fn() {
    f();
}

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

fn call_fn_once<F>(f: F) where F: FnOnce() {
    f();
}

struct Data(i32);

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

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

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

例4: キャプチャした変数がCopyを実装している場合

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

例4(例1でDataをCopy可能にした場合)
fn call_fn<F>(f: F) where F: Fn() {
    f();
}

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

fn call_fn_once<F>(f: F) where F: FnOnce() {
    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(|| {
    not_consume(x);
});

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

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

こちらについても、同様の考え方で納得できるかと思います。

補足: moveクロージャについて

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

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

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

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

c();

これは、キャプチャした変数を匿名構造体のメンバとして持つときに、以下のようにmoveの有無によって参照で持つか実体で持つかを変えられる、と解釈できます:

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

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

自分は最初にmoveの説明を聞いた時、所有権と関係するということで、「FnOnceと何か関係があるのか?」と少し混乱しました。
ですが、実際はmoveを付けてもFnOnceFnMutFnのどれが実装されるかには影響しません
すなわち、上の例1~例4は全て、クロージャにmoveを付けてもそのまま成り立ちます。

これは、最初に述べた考え方に沿って考えれば納得できるかと思います。

実際、moveFnOnceは、以下のようにそれぞれ別の段階での所有権の移動に紐づいています。

  • moveは環境からクロージャへの所有権の移動を強制する
  • FnOnceのみ実装されるのはクロージャから外部の関数等への所有権の移動があるとき

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

実際に「クロージャを受け取る関数」を書くときに、どのtraitで受け取るべきかを考えてみます。
上で説明したように、クロージャの間には以下の包含関係があります。

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

これは、もしクロージャを受け取るときにFnで受け取るようにすると、一部のクロージャが受け取れないことを意味します。
使えるクロージャに不必要な制限は設けたくないので、可能な限りFnよりFnMut、またFnMutよりFnOnceでクロージャを受け取るべき、と考えられます。

ということで、基本的には以下のように決めればよいと思います。

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

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

同様に、クロージャを返す時にどのtraitを使うべきか考えます。
まず前提として、前提で述べたようにクロージャの具体的な型はコーディング時には分からないので、クロージャを返す時には、Boxでラップしてトレイトオブジェクトとして返却する必要があります。

fn make_counter(init: i32, inc: i32) -> Box<FnMut() -> i32> {
    let mut x = init;
    // Boxに入れて返却
    // move を付けないとエラーになるので注意
    Box::new(move || {
        x += inc;
        x - inc
    })
}

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

今度は引数で受け取るときとは逆に、「FnFnMutでもあり、FnMutFnOnceでもある」ことを考えると、可能な限りFnOnceよりもFnMut、またFnMutよりもFnとして返したほうが、呼び出し側での融通が効いて良いと思われます。
(なお、FnOnceはそのままBoxに入れても呼び出せないので注意。参考

参考文献

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

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.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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
ユーザーは見つかりませんでした