はじめに
高度な関数とクロージャへようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習できます。
この実験では、関数ポインタやクロージャの返却など、関数とクロージャの高度な機能を探ります。
高度な関数とクロージャ
このセクションでは、関数ポインタやクロージャの返却など、関数とクロージャに関連するいくつかの高度な機能を探ります。
関数ポインタ
クロージャを関数に渡す方法については既に説明しましたが、通常の関数も関数に渡すことができます!この技術は、既に定義した関数を渡したい場合、新しいクロージャを定義する代わりに便利です。関数は、大文字の F ではなく、小文字の f を持つ fn 型に暗黙的に変換されます。これは、Fn クロージャトレイトと混同しないようにしてください。fn 型は、関数ポインタ と呼ばれます。関数ポインタを使って関数を渡すことで、関数を他の関数の引数として使うことができます。
パラメータが関数ポインタであることを指定する構文は、クロージャの構文と似ています。例えば、add_one という関数を定義しており、この関数は引数に 1 を加えるものとします。do_twice 関数は 2 つのパラメータを持ちます。i32 型のパラメータを取り、i32 を返す任意の関数への関数ポインタと、1 つの i32 型の値です。do_twice 関数は、関数 f に arg 値を渡して 2 回呼び出し、その 2 つの関数呼び出しの結果を加えます。main 関数は、add_one と 5 を引数に do_twice を呼び出します。
ファイル名:src/main.rs
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {answer}");
}
リスト 19-27: fn 型を使って関数ポインタを引数として受け取る
このコードは The answer is: 12 と出力します。do_twice のパラメータ f は、i32 型の 1 つのパラメータを取り、i32 を返す fn であることを指定しています。その後、do_twice の本体で f を呼び出すことができます。main 関数では、関数名 add_one を do_twice の最初の引数として渡すことができます。
クロージャとは異なり、fn はトレイトではなく型なので、ジェネリック型パラメータを Fn トレイトの 1 つをトレイト境界として宣言する代わりに、直接パラメータ型として fn を指定します。
関数ポインタは、すべての 3 つのクロージャトレイト (Fn, FnMut, および FnOnce) を実装しています。これは、関数ポインタを常にクロージャを期待する関数の引数として渡すことができることを意味します。関数を書く際には、ジェネリック型とクロージャトレイトの 1 つを使うことが望ましいです。これにより、関数は関数とクロージャの両方を受け付けるようになります。
ただし、fn のみを受け付け、クロージャを受け付けない場合の 1 つの例は、クロージャがない外部コードとのインターフェイスを行う場合です。C 言語の関数は関数を引数として受け付けることができますが、C にはクロージャはありません。
インラインで定義したクロージャまたは名前付き関数のどちらを使うかの例として、標準ライブラリの Iterator トレイトによって提供される map メソッドの使い方を見てみましょう。数値のベクタを文字列のベクタに変換するために map 関数を使う場合、クロージャを使うことができます。
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> = list_of_numbers
.iter()
.map(|i| i.to_string())
.collect();
あるいは、クロージャの代わりに名前付き関数を map の引数として指定することもできます。
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> = list_of_numbers
.iter()
.map(ToString::to_string)
.collect();
to_string という名前の関数が複数あるため、「高度なトレイト」で説明した完全修飾構文を使う必要があることに注意してください。
ここでは、ToString トレイトに定義された to_string 関数を使っています。このトレイトは、Display を実装する任意の型に対して標準ライブラリによって実装されています。
「列挙型の値」で思い出してください。定義した各列挙型のバリアントの名前も初期化関数になります。これらの初期化関数は、クロージャトレイトを実装する関数ポインタとして使うことができます。これは、初期化関数をクロージャを取るメソッドの引数として指定できることを意味します。
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> = (0u32..20)
.map(Status::Value)
.collect();
ここでは、map が呼び出される範囲内の各 u32 値を使って、Status::Value の初期化関数を使って Status::Value インスタンスを作成しています。このスタイルが好きな人もいれば、クロージャを使う人もいます。どちらのコードも同じコードにコンパイルされるので、どちらのスタイルが分かりやすいか使ってください。
クロージャの返却
クロージャはトレイトによって表されるため、直接クロージャを返すことはできません。トレイトを返したい場合のほとんどのケースでは、代わりにトレイトを実装する具体的な型を関数の返却値として使うことができます。しかし、クロージャの場合はそれができません。なぜなら、返却可能な具体的な型がないからです。例えば、関数ポインタ fn を返却型として使うことはできません。
次のコードは、直接クロージャを返そうとしていますが、コンパイルされません。
fn returns_closure() -> dyn Fn(i32) -> i32 {
|x| x + 1
}
コンパイラのエラーは次の通りです。
error[E0746]: return type cannot have an unboxed trait object
--> src/lib.rs:1:25
|
1 | fn returns_closure() -> dyn Fn(i32) -> i32 {
| ^^^^^^^^^^^^^^^^^^ doesn't have a size known at
compile-time
|
= note: for information on `impl Trait`, see
<https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-
implement-traits>
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of
type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32`
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
| ~~~~~~~~~~~~~~~~~~~
エラーは再び Sized トレイトを参照しています!Rust は、クロージャを格納するのにどれだけのスペースが必要かを知りません。この問題の解決策を前に見たことがあります。トレイトオブジェクトを使うことができます。
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
このコードは正常にコンパイルされます。トレイトオブジェクトに関する詳細は、「異なる型の値を許容するトレイトオブジェクトの使用」を参照してください。
次に、マクロを見てみましょう!
まとめ
おめでとうございます!高度な関数とクロージャの実験を完了しました。LabEx でさらに多くの実験を行って、スキルを向上させることができます。