Skip to content

Instantly share code, notes, and snippets.

@yutopp
Last active November 9, 2017 07:44
Show Gist options
  • Save yutopp/e81ebade3c11337194ae081ec1381af6 to your computer and use it in GitHub Desktop.
Save yutopp/e81ebade3c11337194ae081ec1381af6 to your computer and use it in GitHub Desktop.

Rust RFC読み会: 1558-closure-to-fn-coercion

RFC: 1558-closure-to-fn-coercion.md
Rustには1.19.0で導入

概要

moveやborrow、またはローカル変数にその他のアクセス(キャプチャ)をしないクロージャは、関数ポインタ(fn)に暗黙変換可能であるべき。

モチベーション

今のRustでは、事前に定義済みの関数以外のなにもかもは関数ポインタとして束縛することが不可能である。クロージャを扱うときは、Rustの型推論に頼るか、ある型シグネチャでクロージャを抽象化するためにFnトレイトを使うことになる。

関数を定義して定義して、それとと同時にそれを関数ポインタに束縛することは出来ない。

コレは、明白に、便利みに動機つけされた機能で、この方法でコードを束縛できないシチュエーションにおいては、めっちゃボイラープレートを作ることになる。例えば、小さく単純だが固有の関数をもつ小さな配列を作ろうとするとき、全ての関数を前もって個々に事前に定義済みにする必要がある。

fn inc_0(var: &mut u32) {}
fn inc_1(var: &mut u32) { *var += 1; }
fn inc_2(var: &mut u32) { *var += 2; }
fn inc_3(var: &mut u32) { *var += 3; }

const foo: [fn(&mut u32); 4] = [
  inc_0,
  inc_1,
  inc_2,
  inc_3,
];

これは些細な例で、そこまで重大に見えないかもしれないが、このコードは配列に追加された全ての項において2重になってしまう。

解決方法は、もちろん、fnのかわりにFnの配列を使うことである。

const foo: [&'static Fn(&mut u32); 4] = [
  &|var: &mut u32| {},
  &|var: &mut u32| *var += 1,
  &|var: &mut u32| *var += 2,
  &|var: &mut u32| *var += 3,
];

これは問題を解決したようにみえる。まあ、残念ながら、Fnトレイトの参照を使ったことで、これではfoo[n](&mut bar)を実行する際に余計なレイヤの間接参照が追加されてしまう。

キャプチャをするクロージャは、キャプチャされた変数への参照を保持している構造体にすぎない。Rustは、このようなケースでは必ず動的ディスパッチを用いる必要がある。そして、このコードは構造体内に格納された参照にアクセス可能であるクロージャを伴っている。

この関数ポインタ配列が特にhot codeにあるシチューエーションでは、最適化が望まれる。一般に、不必要な間接参照は排除されることが好ましい。そしてもちろん、FFIを扱うときはこの構文を用いることはできない。

細かいコード量の話とは別として、無名関数はそれは真にプログラマにとって便利なものであろう。コールバックを多用する場合、例えば、利用箇所からはみだしたところで関数を定義することは非現実的であるし、それぞれに混乱アンド不必要な名前をつけることが要求されてしまう。一番最初の例で考えると、inc_Xという名前がはみだした関数に使われているが、これより複雑なことをする関数の表現は簡単ではないだろう。

最後に、この種類の自動な暗黙の型変換は、プログラマにとって単純に直感的だろう。&Fnの例では、クロージャによって変数はキャプチャされていないため、これらを無名関数としてコンパイラが扱うことを止めるものはいないという意見である。

詳細設計

C++では、キャプチャしていないラムダ(C++でクロージャに相当)はなんらかの変数をキャプチャする必要がないときは関数ポインタに"decay" intoする。例えば、Cの関数にラムダを渡すために使われる。

void foo(void (*foobar)(void)) {
    // impl
}
void bar() {
    foo([]() { /* do something */ });
}

この提案があれば、Rustユーザは同じことができる。

fn foo(foobar: fn()) {
    // impl
}
fn bar() {
    foo(|| { /* do something */ });
}

モチベーションにあった例を用いると、このコードの配列はパフォーマンスの問題なしに単純化できる。

const foo: [fn(&mut u32); 4] = [
  |var: &mut u32| {},
  |var: &mut u32| *var += 1,
  |var: &mut u32| *var += 2,
  |var: &mut u32| *var += 3,
];

fn型を直接生成する項は言語に存在しないため、それぞれのfn項は具体化のための過程を通る必要がある。暗黙の型変換を行うために、rustcは加えてサイズが未定なクロージャをfn型に具体化できるようにならなければならない。これの実装は、クロージャのキャプチャの情報はトップレベルに記録されているという事実から単純にできる。

注記: 一度明示的にFnトレイトに代入されると、そのクロージャはもうfnに暗黙の型変換はできない。たとえキャプチャがないとしてもである。

let a: &Fn(u32) -> u32 = |foo: u32| { foo + 1 };
let b: fn(u32) -> u32 = *a; // Can't re-coerce

欠点

この提案では、Rustユーザが不意にAPIを制約できてしまうような可能性がある。crateのような場面で、Fnの代わりにfnを返しているユーザは、最初はコードをコンパイルすることを考えるが、後々変数のキャプチャが必要になったとき壊してしまう。

// The specific syntax is more convenient to use
fn func_specific(&self) -> (fn() -> u32) {
  || return 0
}

fn func_general<'a>(&'a self) -> impl Fn() -> u32 {
  move || return self.field
}

上の例では、APIの作者は関数の特殊バージョンから始めて、後々ある機会で変数のキャプチャを必要にできる。これは破壊的変更になりうるfnからFnへの変更を要求する。

我々はcrateの作者が、&self&mut selfをとるかどうかを決定するときのように、APIの柔軟性を他の領域から測定することを期待する。上記に似たシチューエーションの例を挙げる。

fn func_specific<'a>(&'a self) -> impl Fn() -> u32 {
  move || return self.field
}
    
fn func_general<'a>(&'a mut self) -> impl FnMut() -> u32 {
  move || { self.field += 1; return self.field; }
}

この側面は、提案による変更からくる便利さや単純化、最適化のポテンシャルにより、おそらく凌駕されるだろう。

代替手段

関数リテラル構文

(疲れてきた)

関数リテラル追加してメッチャええやんと思ったけど、戻り値の型推論みたいな新しい意味をfn構文に追加する必要とかあって微妙だし、クロージャの暗黙の変換のほうがいいのでは、みたいなことを言っていると思います。

↓例になってる構文

let foo = fn() { /* do something */ };
foo();
const foo: [fn(&mut u32); 4] = [
  fn(var: &mut u32) {},
  fn(var: &mut u32) { *var += 1 },
  fn(var: &mut u32) { *var += 2 },
  fn(var: &mut u32) { *var += 3 },
];

↓型がしんどい例

fn(x: bool) { !x }

アグレッシブな最適化

Fnトレイトを使っていても静的解析とかで頑張って最適化をかまして間接参照とか無くしたいけどしんどいよね、最適化のプロセスも複雑になるし逆アセンブルとかしないと実際に最適化されたか分からんのは大変でしょ、みたいなことを言っていると思います。

未解決の質問

将来的に将来的に、この振る舞いをより一般化すれば、Fnトレイトを実装したゼロサイズ型はfnポインタに変換可能になるか?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment