第13回

第13章 関数型言語の機能 イテレータとクロージャ

2020/10/30 原山和之


本日のメニュー

  1. クロージャ、変数に保存できる関数に似た文法要素
  2. イテレータ、一連の要素を処理する方法
  3. これら2つの機能を使用して第12章の入出力プロジェクトを改善する方法


クロージャ

Rustのクロージャは、変数に保存したり、引数として他の関数に渡すことのできる匿名関数です。ある場所でクロージャを生成し、それから別の文脈でクロージャを呼び出して評価することができます。 関数と異なり、呼び出されたスコープの値をクロージャは、キャプチャすることができます。


動作の抽象化

とあるアプリのアルゴリズム。。。

アプリユーザの年齢や、 BMI、運動の好み、最近のトレーニング、指定された強弱値などの多くの要因からトレーニングプランを生成するアルゴリズムあるとします。


架空のメソッド

処理に2秒かかる強度を返すメソッド
use std::thread; use std::time::Duration; // 重い計算と仮定する fn simulated_expensive_calculation(intensity: u32) -> u32 { // ゆっくり計算します println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); intensity }

プラン生成

ユーザがトレーニングプランを要求した時にアプリが呼び出すコード
fn main() { // 簡潔にするために一部ハードコード // ハードコードされたユーザー入力の強弱値 let simulated_user_specified_value = 10; // ハードコードされたランダム値(rand) let simulated_random_number = 7; generate_workout( simulated_user_specified_value, simulated_random_number ); }

ビジネスロジック部分

fn generate_workout(intensity: u32, random_number: u32) { if intensity < 25 { println!( // 今日は{}回腕立て伏せをしてください! "Today, do {} pushups!", simulated_expensive_calculation(intensity) ); println!( // 次に、{}回腹筋をしてください! "Next, do {} situps!", simulated_expensive_calculation(intensity) ); } else { if random_number == 3 { // 今日は休憩してください!水分補給を忘れずに! println!("Take a break today! Remember to stay hydrated!"); } else { println!( // 今日は、{}分間走ってください! "Today, run for {} minutes!", simulated_expensive_calculation(intensity) ); } } }

関数でリファクタリング

// 重複削除して一つにする fn generate_workout(intensity: u32, random_number: u32) { let expensive_result = simulated_expensive_calculation(intensity); if intensity < 25 { println!( "Today, do {} pushups!", expensive_result ); println!( "Next, do {} situps!", expensive_result ); } else { if random_number == 3 { println!("Take a break today! Remember to stay hydrated!"); } else { println!( "Today, run for {} minutes!", expensive_result ); } } }

クロージャーを使用する

let expensive_closure = |num| { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num };


クロージャーでリファクタリング

fn generate_workout(intensity: u32, random_number: u32) { let expensive_closure = |num| { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; if intensity < 25 { println!( "Today, do {} pushups!", expensive_closure(intensity)//-> 同じクロージャ1 ); println!( "Next, do {} situps!", expensive_closure(intensity)//-> 同じクロージャ2 ); } else { if random_number == 3 { println!("Take a break today! Remember to stay hydrated!"); } else { println!( "Today, run for {} minutes!", expensive_closure(intensity) ); } } }

クロージャの型推論と注釈

  • クロージャでは、fn関数のように引数の型や戻り値の型を注釈する必要はありませんが、
  • 変数に型注釈できます
let expensive_closure = |num: u32| -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num };

関数との比較

  • 引数に1を加える関数の定義と、 同じ振る舞いをするクロージャの定義の記法
fn add_one_v1 (x: u32) -> u32 { x + 1 } let add_one_v2 = |x: u32| -> u32 { x + 1 }; let add_one_v3 = |x| { x + 1 }; let add_one_v4 = |x| x + 1 ;

エラーになるクロージャ

let example_closure = |x| x; let s = example_closure(String::from("hello")); let n = example_closure(5);
error[E0308]: mismatched types --> src/main.rs | | let n = example_closure(5); | ^ expected struct `std::string::String`, found integral variable | = note: expected type `std::string::String` found type `{integer}`

ジェネリック引数とFnトレイトを使用してクロージャ保存

  • トレーニング生成アプリの問題
    • コードは必要以上の回数(二回呼ばれてる)
    • 重い計算のクロージャ

解決方法

  • メモ化(memoization)または、 遅延評価(lazy evaluation)

準備-Cacher構造体

struct Cacher<T> where T: Fn(u32) -> u32 { calculation: T, value: Option<u32>, } impl<T> Cacher<T> where T: Fn(u32) -> u32 { fn new(calculation: T) -> Cacher<T> { Cacher { calculation, value: None, } } fn value(&mut self, arg: u32) -> u32 { match self.value { Some(v) => v, None => { let v = (self.calculation)(arg); self.value = Some(v); v }, } } }

Cacherを使用

fn generate_workout(intensity: u32, random_number: u32) { let mut expensive_result = Cacher::new(|num| { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }); if intensity < 25 { println!( "Today, do {} pushups!", expensive_result.value(intensity) ); println!( "Next, do {} situps!", expensive_result.value(intensity) ); } else { if random_number == 3 { println!("Take a break today! Remember to stay hydrated!"); } else { println!( "Today, run for {} minutes!", expensive_result.value(intensity) ); } } }

Cacherの限界

  • 再利用することを困難にしてしまう問題
#[test] fn call_with_different_values() { let mut c = Cacher::new(|a| a); let v1 = c.value(1); let v2 = c.value(2); assert_eq!(v2, 2); }
thread 'call_with_different_values' panicked at 'assertion failed: `(left == right)` left: `1`, right: `2`', src/main.rs
  • 単独の値ではなく、ハッシュマップを保持するようにCacherを改変

クロージャの機能

  • クロージャーには、関数にない機能がある
    • 環境をキャプチャし、 自分が定義されたスコープの変数にアクセスできる
fn main() { let x = 4; let equal_to_x = |z| z == x; let y = 4; assert!(equal_to_x(y)); }


イテレータ生成

let v1 = vec![1, 2, 3]; let v1_iter = v1.iter();
  • イテレータを使い込んで消費するメソッドを呼ぶまで何の効果もない。


イテレータ使用

let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { // {}でした println!("Got: {}", val); }

Iteratorトレイトとnextメソッド

  • 標準ライブラリで定義
pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // デフォルト実装のあるメソッドは省略 // methods with default implementations elided }

[](Iteratorトレイトを実装するには、Item型も定義する必要があり、 そして、このItem型がnextメソッドの戻り値の型に使われている。type, Self::Itemは、関連型についての詳細は、第19章)


イテレータを直接呼ぶ

  • イテレータの消費
#[test] fn iterator_demonstration() { let v1 = vec![1, 2, 3]; let mut v1_iter = v1.iter(); assert_eq!(v1_iter.next(), Some(&1)); assert_eq!(v1_iter.next(), Some(&2)); assert_eq!(v1_iter.next(), Some(&3)); assert_eq!(v1_iter.next(), None); }


イテレータの消費

  • 標準ライブラリにデフォルトメソッドがある。
#[test] fn iterator_sum() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); let total: i32 = v1_iter.sum(); assert_eq!(total, 6); }

イテレータアダプタ

  • 新しいイテレータを生成
let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1);


イテレータアダプタ

  • 警告なし
let v1: Vec<i32> = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); assert_eq!(v2, vec![2, 3, 4]);

イテレータとクロージャ

#[derive(PartialEq, Debug)] struct Shoe { size: u32, style: String, } fn shoes_in_my_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> { shoes.into_iter() .filter(|s| s.size == shoe_size) .collect() } #[test] fn filters_by_size() { let shoes = vec![ Shoe { size: 10, style: String::from("sneaker") }, Shoe { size: 13, style: String::from("sandal") }, Shoe { size: 10, style: String::from("boot") }, ]; let in_my_size = shoes_in_my_size(shoes, 10); assert_eq!( in_my_size, vec![ Shoe { size: 10, style: String::from("sneaker") }, Shoe { size: 10, style: String::from("boot") }, ] ); }

Iteratorトレイトで独自のイテレータを作成

  • 構造体
  • 絶対に1から5をカウントするだけのイテレータ
struct Counter { count: u32, } impl Counter { fn new() -> Counter { Counter { count: 0 } } } // イテレータを使用したときに起きてほしいことを書く impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { self.count += 1; if self.count < 6 { Some(self.count) } else { None } } }

他のイテレータメソッドも使用

#[test] fn using_other_iterator_trait_methods() { let sum: u32 = Counter::new().zip(Counter::new().skip(1)) .map(|(a, b)| a * b) .filter(|x| x % 3 == 0) .sum(); assert_eq!(18, sum); }


12章の入出力を改善する-Config

  • Config構造体があったかと。それを変更。
impl Config { pub fn new(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } // ここが問題で非効率 マジックナンバーだしね。。。 let query = args[1].clone(); let filename = args[2].clone(); // ここ追加してる? let case_sensitive = env::var("CASE_INSENSITIVE").is_err(); Ok(Config { query, filename, case_sensitive }) } }


12章の入出力を改善する-main関数

fn main() { let args: Vec<String> = env::args().collect(); let config = Config::new(&args).unwrap_or_else(|err| { eprintln!("Problem parsing arguments: {}", err); process::exit(1); }); // --snip-- }

fn main() { let config = Config::new(env::args()).unwrap_or_else(|err| { eprintln!("Problem parsing arguments: {}", err); process::exit(1); }); // --snip-- }


12章の入出力を改善する-Config

impl Config { pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> {

Iteratorトレイトメソッドを使う

  • newメソッド
impl Config { pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> { args.next(); let query = match args.next() { Some(arg) => arg, // クエリ文字列を取得しませんでした None => return Err("Didn't get a query string"), }; let filename = match args.next() { Some(arg) => arg, // ファイル名を取得しませんでした None => return Err("Didn't get a file name"), }; let case_sensitive = env::var("CASE_INSENSITIVE").is_err(); Ok(Config { query, filename, case_sensitive }) } }

前のやつ


Iteratorトレイトメソッドを使う

  • searchメソッド
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { contents.lines() .filter(|line| line.contains(query)) .collect() }

前のやつ


ループVSイテレータ

  • サー・アーサー・コナン・ドイル(Sir Arthur Conan Doyle)の、 シャーロックホームズの冒険(The Adventures of Sherlock Homes)全体をStringに読み込み、 そのコンテンツでtheという単語を検索することでベンチマークを行いました。 こちらが、forを使用したsearch関数のバージョンと、イテレータを使用したバージョンに関するベンチマーク結果です。
test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700) test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)


Select a repo