🦀

【Rust】クロージャと関数ポインタ

に公開

今回の記事はRustのクロージャについて。他の多くの言語と同様、Rustにもクロージャや関数ポインタを扱うための構文が用意されています。ただし、Rustには所有権やトレイトなどの独自の機能が存在するため、これらとの関わりも理解する必要があるでしょう。

この記事ではRustにおけるクロージャ・関数ポインタの構文から、これらが内部ではどのように動作しているかを含めて解説していきます。

クロージャ

Rustでは以下のような構文でクロージャを作成することができます。

let double = |a| a * 2;

let sub = |a, b| {
    a - b
};

||で囲われた内部に引数を記述し、その後に式を定義します。これらのクロージャは通常の関数と同様に呼び出すことが可能です。

println!("{}", double(2)); // 4
println!("{}", sub(1, 2)); // -1

引数や戻り値の型は推論されますが、明示的に記述することも可能です。

let sub = |a: i32, b: i32| -> i32 {
    a - b
};

扱い方は概ね通常の関数と変わりありませんが、ジェネリクスやライフタイム注釈を利用することはできません。 とはいえクロージャでこれが必要になることは基本的にないため、あまり問題にはならないでしょう。

外部変数のキャプチャ

Rustのクロージャはその名前の通り、外部変数をキャプチャすることが可能です。 例えば、以下のコードではクロージャ外部のローカル変数であるxを参照し、その値をprintln!()で出力しています。

let x = "hello".to_string();

let f = || {
    // クロージャ内部で外部のローカル変数xを参照する
    println!("{}", x);
};

f(); // hello!

ただし、クロージャ内でmutな外部変数を変更する場合は、クロージャもmutである必要があります。

let mut x = 10;

// クロージャもmut変数にする
let mut f = || {
    // 内部でxを書き換え
    x = 20;
};

f();
println!("{}", x); // 20

クロージャと所有権

クロージャで何らかの変数をキャプチャした場合、デフォルトではクロージャは変数を借用する動作になります。

let x = "hello".to_string();

// このクロージャはxを借用する
let f = || {
    println!("{}", x);
};

そのため、以下のような状況ではライフタイムに起因するコンパイルエラーが発生します。

// クロージャを作成する関数
fn create_closure() -> impl Fn() -> () {
    let x = "hello".to_string();
    
    // 外部変数xをキャプチャしたクロージャを作成
    // このクロージャはxを借用する
    let f = || {
        println!("{}", x);
    };
    
    // fは戻り値として返されるが、その時点でxのスコープ外であるためコンパイルエラー
    // error[E0373]: closure may outlive the current function, but it borrows `x`, which is owned by the current function
    return f;
}

これを解決するには、moveキーワードを用いて明示的に変数の所有権を移動させる必要があります。以下のようにコードを変更することで、コンパイルが通るようになります。

fn create_closure() -> impl Fn() -> () {
    let x = "hello".to_string();
    
    // xの所有権をクロージャに移動させる
    let f = move || {
        println!("{}", x);
    };
    
    // xの所有権が移動されているのでOK
    return f;
}

当然ですがmoveを使うと所有権がクロージャ内部に移動するため、Copyトレイトを実装していない型の場合は以後利用することができなくなります。

let x = "hello".to_string();

// ここで所有権が移動
let f = move || {
    println!("{}", x);
};

// そのためこれはコンパイルエラー
// error[E0382]: borrow of moved value: `x`
println!("{}", x);

FnOnce、FnMut、Fnトレイト

さて、ここまではクロージャの使い方を見ていきましたが、実際にこれらのクロージャはどのように実装されているのでしょうか。

Rustのコンパイラはクロージャに対し、FnOnceFnMutFnトレイトのうち可能なもの全てを実装した構造体を生成します。定義は概ね以下のような感じです。

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

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

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

実際にどのトレイトが実装されるかどうかは、クロージャの定義によって異なります。順番に見ていきましょう。

FnOnce

FnOnce一度だけの呼び出しが可能なクロージャを表すトレイトです。これは全てのクロージャが実装しています。

キャプチャした変数の所有権がクロージャ外部へ移動している場合、クロージャはFnOnceのみを実装します。例えば以下のような場合。

fn main() {
    let x = "x".to_string();

    let f = move || {
        // xの所有権がgoodbye関数(クロージャ外部)に移動する
        goodbye(x);
    };
}

fn goodbye(x: String) {
    println!("goodbye, {}!", x);
}

このクロージャは一度呼び出されたらxをDropするため、二度呼び出すことができません。

f(); // 一度目はOK
f(); // 二度呼ぶとコンパイルエラー (error[E0382]: use of moved value: `f`)

そのため、このときのfFnOnceのみを実装することになります。

FnMut

FnMut内部で何らかの変更を行うクロージャに対して実装されるトレイトです。FnOnceとは異なり、これは何度でも呼び出すことが可能です。

クロージャが可変参照を借用する場合、そのクロージャはFnOnceに加えてFnMutも実装します。

let mut x = 0;

// これはFnMutを実装する
let mut f = || {
    x += 1;
};

Fn

Fn内部で変数をキャプチャしないか、不変な参照を借用するクロージャに対して実装されるトレイトです。こちらも何度でも呼び出しが可能です。

以下のようなクロージャはFnOnceFnMutFnの全てを実装します。

// これらはFnを実装している
let f = || {
    println!("hello!");
};

let x = 0;
let g = || {
    println!("{}", x);
}

クロージャを引数として受け取る

クロージャはトレイトで表現されるため、直接引数の型に指定することができません。

// これはコンパイルエラー
// error[E0782]: expected a type, found a trait
fn call_closure(f: Fn()) {
    f();
}

クロージャを引数として受け取る関数を定義する場合は、上のトレイトのいずれかを制約にもつジェネリクス関数として定義します。

// 受け取ったクロージャを呼び出し、その結果を返すだけの関数
fn call_closure<T, U>(f: T) 
    where T: Fn() -> U {
    f()
}

// 引数の型も指定できる
fn call_closure_with_args<T, U>(f: T, args: U)
    where T: Fn(U) {
    f(args);
}

また、トレイトオブジェクトとしてクロージャを受け取ることも可能です。

fn call_closure(f: Box<dyn Fn()>) {
    f();
}

クロージャを返す

先ほどのサンプルコードで既に登場していましたが、クロージャを戻り値として返す場合にも一工夫必要になります。

最も簡単なのはimpl traitを用いる方法です。

fn create_closure() -> impl Fn() {
    || { 
        println!("hello");
    }
}

ただし、クロージャは匿名型であるため、以下のように複数のクロージャがあるケースではコンパイルが通りません。

// これはコンパイルエラー
// error[E0308]: mismatched types
fn create_is_zero(x: isize) -> impl Fn() {
    if x == 0 {
        return || { 
            println!("x is 0");
        };
    } else {
        return move || { 
            println!("x is not 0. x is {}", x);
        };
    }
}

このような場合にはトレイトオブジェクトで返す必要があります。

fn create_is_zero(x: isize) -> Box<dyn Fn()> {
    if x == 0 {
        Box::new(|| { 
            println!("x is 0");
        })
    } else {
        Box::new(move || { 
            println!("x is not 0. x is {}", x);
        })
    }
}

関数ポインタ

Rustにはクロージャだけでなく、純粋な関数ポインタも用意されています。

関数ポインタはfn(T, U) -> Vのような型で表され、以下のような形で利用することが可能です。

fn main() {
    // addを関数ポインタとして渡す
    let x = call_fn_ptr(add);

    println!("{}", x); // 3
}

fn add(a: i32, b: i32) -> i32 {
    a + b
}

// 関数ポインタを受け取り、(1, 2)を渡して呼び出した結果を返す
fn call_fn_ptr(f: fn(i32, i32) -> i32) -> i32 {
    f(1, 2)
}

関数ポインタ → クロージャ

Rustの関数ポインタはFnOnceFnMutFnトレイトを全て実装しています。そのためクロージャを受け取る関数に関数ポインタを渡すことも可能です。

fn main() {
    // 関数ポインタをcall_closureに渡す
    call_closure(foo); // foo
}

fn foo() {
    println!("foo");
}

fn call_closure<T: Fn()>(f: T) {
    f();
}

クロージャ → 関数ポインタ

変数を一切キャプチャしないクロージャであれば関数ポインタの取得が可能です。(変数をキャプチャしているクロージャを関数ポインタに変換することはできません。)

// 変数をキャプチャしないクロージャ
let add = |a, b| a + b;

// 関数ポインタとして渡すことが可能
call_fn_ptr(add);

関数定義型

関数そのものの型は正確には関数ポインタ型ではなく、関数定義型と呼ばれるものになります。

以下のコードは関数のシグネチャが一致しているため問題ないように見えますが、実際はコンパイルエラーになります。

fn main() {
    let mut f = foo;

    // これはコンパイルエラー
    // expected fn item `fn() {foo}`
    // found fn item `fn() {bar}`
    f = bar;
}

fn foo() { 
    println!("foo");
}

fn bar() {
    println!("bar");
}

ここでエラーに表示されるfn item `fn() {foo}`が関数定義型です。これは関数ごとに割り当てられる内部型になっています。

通常、関数定義型は関数ポインタに暗黙的な変換が行われるため上のような問題が起こることはないですが、まれに型の違いからコンパイルエラーとなるケースがあるため、覚えておくと良いでしょう。

まとめ

というわけで、クロージャと関数ポインタについてのまとめでした。Rust特有の所有権やトレイトなどの機能はクロージャの動作にも影響するため、どのような仕組みで動いているかを理解することは重要です。クロージャは非常に強力な機能であるため、是非とも使いこなしていきましょう。

Discussion