高階関数は、他の関数を引数として受け取ったり、結果として返したりする関数です。Rustはその開発の初期段階から型安全性とパフォーマンスを重視しており、このような関数の取り扱いに反映されています。
背景:
関数型言語では高階関数が標準とみなされていますが、多くのシステムプログラミング言語では、パフォーマンスの低下を引き起こすことがよくあります(例えば、アロケーションによるものや、コードを「インライン化」できないことなど)。Rustでは、厳格な型システム、静的ディスパッチ、またはトレイト(Fn, FnMut, FnOnce)を通じてこの機能が実装されており、大多数のケースでオーバーヘッドを回避することができます。
問題点:
主な問題は、型安全性を保ちながら関数やクロージャを渡し、変数のキャプチャ(ラムダ式の軽さ)とアロケーションや仮想呼び出しなしのパフォーマンスを両立させる必要があることです。
解決策:
Rustでは高階関数がジェネリクスパラメータと関数/クロージャ用のトレイトラッパーを介して実装されています。標準トレイトのFn, FnMut, FnOnceは、渡される関数に対して明確な要件を宣言することを可能にします(それが状態を変更できるか、環境を消費するかどうか)。ジェネリクスを介した引き渡しにより、コンパイル時に呼び出しをインライン化できます。また、事前に型がわからない場合に対応するために、Box<dyn Fn...>を通じた動的ディスパッチもあります。
コード例:
fn apply_to_vec<F: Fn(i32) -> i32>(v: Vec<i32>, f: F) -> Vec<i32> { v.into_iter().map(f).collect() } let nums = vec![1, 2, 3]; let doubled = apply_to_vec(nums, |x| x * 2); // doubled == [2, 4, 6]
主な特徴:
Fn、FnMut、FnOnceの違いは何ですか?
多くの人は、これらが構文の違いだけであるとか、FnとFnMutがすべて互換的に機能すると思っています。実際には:
FnOnceは一度だけ呼び出すことができます(例: ラムダがキャプチャした値を移動する場合など)。FnMutはキャプチャされた環境の状態を変更できますが、何度も呼び出すことができます。Fnは環境を変更しません。例:
let mut sum = 0; let mut add = |x| { sum += x; }; // addはFnMutを実装していますが、Fnではありません
ボックスなしで関数を値として渡すことはできますか?
多くの人は、関数引数は必ずボックス化(Box<dyn Fn...>)されるべきだと思っていますが、実際には動的ディスパッチに必要な場合のみボクシングが必要です。ジェネリクスパラメータを通じて、関数は完全に静的に型付けされ、アロケーションやボックスなしで実行できます。
クロージャがCopyでなくなるのはいつですか?
いくつかの人は、内部の変数がCopyであれば、単純なクロージャも常にCopyまたはCloneであると考えています。実際には、クロージャはデフォルトでCopyではなく、たとえキャプチャされた変数がCopyであってもそうです。トレイトを明示的に実装するか、単純な関数で済ませる必要があります。
プロジェクトでは、すべてのコールバックにBox<dyn Fn()>を使用し、インライン化やアロケーションについて考えませんでした。その結果、パフォーマンスの向上は得られず、頻繁なアロケーションが遅延を引き起こしました。
プラス:
マイナス:
イベントハンドラーは、トレイトの制約FnMutを持つジェネリック関数を介して設定され、アロケーションなしで完全に済みました。
プラス:
マイナス: