/tmp

雑記帳.

FnBox

Rust Book 勉強会 #4 で発表をしてきた。ジェネリクスとトレイトについての章を担当したが、まあ、かなり重かった。本当はライフタイムの節までやる予定だったが、完全に時間がなかった。私は別に何時間でも話していられるのだが、私自身が何週間もかけて理解したことを2時間ほどで凝縮したわけなので、参加者の皆さんへの負担はかなり高かったと思う。昔は所有権とライフタイム、ジェネリクスとトレイトで分かれていたと思うのだが、この 3 本立てはかなり辛い。

さて、それはともかくとして、最後の方で FnOnce の話が登場した。 FnOnce は関数呼出演算子を提供する3つのトレイト Fn, FnMut, FnOnce の一つである。名前から分かるように、 FnOnce は一度だけしか呼び出せない関数を表している。この手の関数の例としては、クロージャであって、キャプチャした変数の所有権を利用するようなものが考えられる:

let s = "hello".to_string();
let c = || { drop(s); };

このクロージャs を環境内にキャプチャしている。 drop() は所有権を要求するので、s の実体がないとならない。よって c が定義された段階で、 s を環境内にムーブする。このクロージャを実行するためには s の実体が必要なので、初回の呼出で環境内のs が所有権ごと処理本体にムーブされてくる。結果として、このクロージャは二度呼ぶことができない。

まあ、詳しいことは以前散々書いたので読んでいただけるといい。

statiolake.hatenablog.com

ところで、クロージャのように匿名でユニークな型をもつものは、以前は、関数から直接返す方法がなかった。 Rust では型推論を関数内に敢えて限っており、関数の引数や戻り値は必ず明示的に型を書かなければならない。よって型名を表記できないものは返すことができない。

今日では非常に有り難いことに impl Trait によりこのクロージャが直接返せるようになっている。 impl Traitイテレータを返すのに非常に便利だし、クロージャを返すのに必要な、本当に素晴らしい機能である。今のところ、ある単一のクロージャを返したいときは impl Trait を使うのが一番よい:

fn get_closure() -> impl Fn() -> String {
    || "hello".to_string()
}
let c = get_closure();
println!("{}", c()); // => hello, world

では、以前は関数でクロージャを返したい時はどうしていたか。トレイトオブジェクトというものを使っていた:

fn get_closure() -> Box<dyn Fn() -> String> {
    Box::new(|| "hello".to_string())
}
let c = get_closure();
println!("{}", c());

トレイトオブジェクトをざっくり説明すると そのトレイトを実装した型 という型であって、型名は dyn Trait となる。ただし具体的な型が何なのかによってそのサイズは任意になり得るので Sized ではなく、必ず 参照 &dyn Trait または Box<dyn Trait> の形で扱うことになる。この参照は あるオブジェクト自身へのポインタ仮想関数テーブルへのポインタ をもつ。つまり、オブジェクト指向言語における 派生クラスのポインタを基底クラスのポインタとして見る ようなことである。

トレイトオブジェクトを使う必要性やメリットとしては次のようなことがある。

  1. 具体的な型名が判明していなくても書ける。
  2. そのトレイトを実装していさえすれば、実際の型は異なっていても同じ型として扱える。

しかし、その目的故に次のようなデメリットも目立つ。

  1. メソッド呼び出しなどを必ず動的に解決することになる。
  2. 関数から返す場合は、必ず実体をスタックではなくヒープに確保しなければならないのでそのコストがかかる。
  3. 参照の形でしか扱えないため、&self&mut self を引数に取らない関数は呼び出せない。

特に 3 つ目の制約は大きい。反するのは関連関数 (static 関数) と、 self を取る関数である。関連関数の方は、そもそも Type::function() の形でしか呼び出せないので、具体的な型が分かっていない間は呼び出しようがない。また、 Sized でないものは関数の引数に渡せないので、 self をとる関数も実行できない。

さて、 FnOnce は一度しか実行できないクロージャが実装しているが、それは何故だったか。 FnOnce になるようなクロージャは環境を関数本体へムーブする必要があるからであった。つまり FnOnce の関数呼出演算子を実装するメソッドは self をとっているはずだ。実際、今のところの FnOnce トレイトは次のようになっている(変更される可能性がある):

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

したがって、 FnOnce のトレイトオブジェクトは呼び出すことができない:

fn get_closure() -> Box<dyn FnOnce() -> String> {
    let s = "hello".to_string();
    Box::new(|| s)
}
let c = get_closure();
// c(); // 型のサイズが静的に決定できないため、ムーブできずにエラー。

これができるようになる機能は現在 unsized_locals として実装されているところである。実は nightly であれば unsized_locals を有効化すると既に実行することができる。ではこれが安定化するまでは、どのようにすれば Box<dyn FnOnce> を呼び出すことができるのか?

その前に、メソッドがとりうる self 型の種類を復習しておこう。というか、それを見ると解決策は一瞬で見えるはずだ:

impl Foo {
    fn by_value(self: Self) {}      // fn by_value(self) {}
    fn by_ref(self: &Self) {}       // fn by_ref(&self) {}
    fn by_mut(self: &mut Self) {}   // fn by_mut(&mut self) {}
    fn by_box(self: Box<Self>) {}   // (省略形なし)
}

上三つは馴染みがあるだろうが、実はなんとメソッドは Box<Self> を取ることもできる:

struct X(i32);
impl X {
    fn by_box(self: Box<Self>) { println!("{}", self.0); }
}

let bx = Box::new(X(3));
bx.by_box(); // => 3

(ちなみに arbitrary_self_types が有効化されると *const SelfRc<Self> など、様々な型が self になれるようになる。)

...ということは、 Fn 系トレイトに Box<Self> をとるものがあれば解決するのではないだろうか?

ある。ただし安定化はされていないので #![feature(fnbox)] が必要になる。トレイト FnBox である:

pub trait FnBox<A> {
    type Output;
    fn call_box(self: Box<Self>, args: A) -> Self::Output;
}

このトレイトは、 F: FnOnce<A> を満たす全ての型 F に対して実装されている:

impl<A, F> FnBox<A> for F
    where F: FnOnce<A> { /* snip */ }

ということで、何のことはなく、 FnOnce ではなく FnBox を使ってやればよかったのだ:

#![feature(fnbox)]
use std::boxed::FnBox;

fn get_closure() -> Box<dyn FnBox() -> String> {
    let s = "hello".to_string();
    Box::new(|| s)
}

fn main() {
    let c = get_closure();
    println!("{}", c()); // 問題なく呼び出せる。
    // c(); // 二回目以降はもちろんエラー。
}

ただし、 FnBox はおそらく安定化されない。理由はこれは Box<dyn FnOnce> が直接実行できない間の代替であって、 unsized_locals が安定化されれば不要になるからである。