Learning Rust - Closures

Syntax and Semantics

Closures

Rustにも言語レベルでclosureの機能があります。 今回はそのclosureを学びます。

Syntax

まず、closureの基本的なsyntaxは以下です。

let plus_one = |x: i32| x + 1;

let plus_two = |x| {
    let mut result: i32 = x;

    result += 1;
    result += 1;

    result
};

fnではなくletで宣言し、引数は||で挟みます。 その後{}内にexpressionを書きますが、一行なら{}を省略できます。 でも省略しないほうがわかりやすいのではないでしょうか。

plus_oneには引数xに型を明示していますが、plus_twoには書いていません。 実は、普通の関数ではできなかった型推論が、closureではできるようになっています。 これは、普通の関数はドキュメントとしても型を明示しておく利点があったのに対し、 closureは一時的なものとしてつくられることが多く、その場合いちいち型を書くのは面倒だから、 というのが理由の一つのようです。 同様に、-> i32のような戻り値の型も省略できます。

Closures and their environment

Closureはclosureなので状態をもてます。 こんな感じです。

let num = 5;
let plus_num = |x: i32| x + num;

ここで、plus_numは、numをimmutableにborrowしています。 よって、次のように、closureでnumをborrowして、さらにmutableにborrowしようとすると、 コンパイルエラーになります。

let mut num = 5;
let plus_num = |x: i32| x + num;

let y = &mut num; // コンパイルエラー

しかし、Vec<T>はtrivially copyableではないので、moveしてclosureに取り込まれます。 よって、closureを作ったあとにそのvectorを使おうとすると、コンパイルエラーになります。 この挙動は普通の関数と同じです。

let nums = vec![1, 2, 3];

let takes_nums = || nums;

println!("{:?}", nums); // コンパイルエラー

move closure

Closureが、ownershipを得ながら状態を取り込むことができます。 moveキーワードをつかいます。

let num = 5;

let owns_num = move |x: i32| x + num;

Moveセマンティクスは、moveする型にあったセマンティクスになります。 つまり、上の例ではi32をmoveしていますが、i32Copytraitをもっているので、 ここではnumをdeep copyしたもののownershipをもつことになります。

ということは、i32をmoveせず取り込む場合は、deep copyではなく、 参照になるということです(この例ではmutableな参照です)。

この違いは下の例でわかります。

let mut num = 5;

{
    let mut add_num = |x: i32| num += x; // not move; num: &mut i32

    add_num(5);
}

assert_eq!(10, num);
let mut num = 5;

{
    let mut add_num = move |x: i32| num += x; // move; num: i32

    add_num(5);
}

assert_eq!(5, num);

違う見かたをすると、move closureは自分自身のスタックフレームをもちますが、 moveしないclosureは、それを作ったスタックフレームとつながっています。 よって、後述するclosureを返す関数は、move closureしか返せないということになります。

Closure implementation

実は、closureは、特別なtraitをimplされたstructのシンタックスシュガーです。 Closureには()が作用できますね。 これはそのためのtraitをimplして、overloadしているからです。

pub trait Fn<Args> : FnMut<Args> {
    extern "rust-call" fn call(&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 FnOnce<Args> {
    type Output;

    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

関数の(型理論的な)型がtraitになっているわけです。 Closureを書くと、環境を取り込んだstructを作り、このFnをimplします。 たしかC++のラムダ式も関数オブジェクトなclassをつくるシンタックスシュガーだった気がします。

Taking closures as argument

さて、closureもtraitをimplしたただのstructであることがわかりました。 ということは、これまでと同じように、何も特別なことなく扱えますね。

例えば、closureをとる関数を考えてみます。

fn call_with_one<F>(some_closure: F) -> i32
    where F : Fn(i32) -> i32 {

    some_closure(1)
}

let answer = call_with_one(|x| x + 2);

assert_eq!(3, answer);

i32をとってi32を返すclosureをとるために、 Fn(i32) -> i32traitをもつ型をとっています。

動的ディスパッチもできます。

fn call_with_one(some_closure: &Fn(i32) -> i32) -> i32 {
    some_closure(1)
}

let answer = call_with_one(&|x| x + 2);

assert_eq!(3, answer);

asで型変換もできますが、&Fn(i32) -> i32と二度書くことにになるので、 上のような書きかたをするのがよさそうです。

Function pointers and closures

普通の関数は、状態をもたないclosureとみなせるので、closureとして他の関数に渡したりできます。

fn call_with_one(some_closure: &Fn(i32) -> i32) -> i32 {
    some_closure(1)
}

fn add_one(i: i32) -> i32 {
    i + 1
}

let answer = call_with_one(&add_one);

assert_eq!(2, answer);

Returning closures

Closureを渡すだけではなく、closureを返すこともできます。 ただし、Rustコンパイラに怒られないように、いくつか注意する必要があります。

結論から言うと、次のようにかけばclosureが返せます。

fn factory() -> Box<Fn(i32) -> i32> {
    let num = 5;

    Box::new(move |x| x + num)
}

let f = factory();

let answer = f(1);
assert_eq!(6, answer);

まず、返すclosureはBox<Fn>の形でboxに包んでいます。 Boxというのは、ヒープ上に確保した、所有者が一つだけのポインタを表しています。 Boxはコンパイル時にlifetimeがきまるので、Rustが自動的に破棄処理をしてくれるようです。

もうひとつ、factory()内でclosureをつくるとき、moveセマンティクスをつかっています。 これは、numfactory()内のスタック変数のため、 moveしないと不正な参照になってしまうからです。 そこで、factory()とは独立した環境にnumを移すため、moveの記述が必要になります。


長かったですね。 今回はclosureについて学びました。 ここまでくると、Rustのsyntaxとsemanticsについて、かなりたくさんのことを覚えた気がしてきます。 しかし、プログラミング言語というものはたくさん書かないとどうしても身につかないものです。 もうちょっと文法を学んだら、たくさんのコードに触れてみようと思います。