impl Trait
impl Trait は、次の 2 つの場所で使用できます。
- 引数型として
- 戻り値の型として
引数型として
関数がトレイトに対してジェネリックであるものの、具体的な型を気にしない場合は、引数の型として impl Trait を使用することで関数宣言を簡略化できます。
たとえば、次のコードを考えてみましょう。
fn parse_csv_document<R: std::io::BufRead>(src: R) -> std::io::Result<Vec<Vec<String>>> { src.lines() .map(|line| { // ソース内の各行について line.map(|line| { // 行を正常に読み取れた場合は処理し、そうでなければエラーを返す line.split(',') // カンマで区切られた行を分割する .map(|entry| String::from(entry.trim())) // 先頭と末尾の空白を削除する .collect() // 行内のすべての文字列を Vec<String> に収集する }) }) .collect() // すべての行を Vec<Vec<String>> に収集する }
parse_csv_document はジェネリックであり、BufReader<File> や [u8] など、BufRead を実装する任意の型を受け取ることができます。 しかし、R がどの型であるかは重要ではなく、R は src の型を宣言するためだけに使用されているため、この関数は次のようにも書けます。
fn parse_csv_document(src: impl std::io::BufRead) -> std::io::Result<Vec<Vec<String>>> { src.lines() .map(|line| { // ソース内の各行について line.map(|line| { // 行を正常に読み取れた場合は処理し、そうでなければエラーを返す line.split(',') // カンマで区切られた行を分割する .map(|entry| String::from(entry.trim())) // 先頭と末尾の空白を削除する .collect() // 行内のすべての文字列を Vec<String> に収集する }) }) .collect() // すべての行を Vec<Vec<String>> に収集する }
引数型として impl Trait を使用すると、関数のどの形式を使用するかを明示的に指定できないことに注意してください。つまり、2 つ目の例では parse_csv_document::<std::io::Empty>(std::io::empty()) は機能しません。
戻り値の型として
関数が MyTrait を実装する型を返す場合、その戻り値の型を -> impl MyTrait と書くことができます。これにより、型シグネチャをかなり簡略化できます。
use std::iter; use std::vec::IntoIter; // この関数は 2 つの `Vec<i32>` を結合し、そのイテレータを返す。 // 戻り値の型がどれほど複雑か見てみよう! fn combine_vecs_explicit_return_type( v: Vec<i32>, u: Vec<i32>, ) -> iter::Cycle<iter::Chain<IntoIter<i32>, IntoIter<i32>>> { v.into_iter().chain(u.into_iter()).cycle() } // これはまったく同じ関数だが、戻り値の型に `impl Trait` を使用している。 // どれほどシンプルになったか見てみよう! fn combine_vecs( v: Vec<i32>, u: Vec<i32>, ) -> impl Iterator<Item=i32> { v.into_iter().chain(u.into_iter()).cycle() } fn main() { let v1 = vec![1, 2, 3]; let v2 = vec![4, 5]; let mut v3 = combine_vecs(v1, v2); assert_eq!(Some(1), v3.next()); assert_eq!(Some(2), v3.next()); assert_eq!(Some(3), v3.next()); assert_eq!(Some(4), v3.next()); assert_eq!(Some(5), v3.next()); println!("all done"); }
さらに重要なことに、一部の Rust の型は書き出すことができません。たとえば、すべての クロージャには、それぞれ固有の名前のない具象型があります。impl Trait 構文以前は、クロージャを返すために ヒープ上に割り当てる必要がありました。しかし現在では、次のようにすべてを 静的に行うことができます。
// 入力に `y` を加算する関数を返す fn make_adder_function(y: i32) -> impl Fn(i32) -> i32 { let closure = move |x: i32| { x + y }; closure } fn main() { let plus_one = make_adder_function(1); assert_eq!(plus_one(2), 3); }
また、impl Trait を使用して、map や filter の クロージャを使用するイテレータを返すこともできます。これにより、map と filter をより簡単に使用できます。クロージャ型には 名前がないため、関数がクロージャを含むイテレータを返す場合、明示的な戻り値の型を書き出すことはできません。 しかし impl Trait を使えば、これを簡単に行えます。
fn double_positives<'a>(numbers: &'a Vec<i32>) -> impl Iterator<Item = i32> + 'a { numbers .iter() .filter(|x| x > &&0) .map(|x| x * 2) } fn main() { let singles = vec![-3, -2, 2, 3]; let doubles = double_positives(&singles); assert_eq!(doubles.collect::<Vec<i32>>(), vec![4, 6]); }