Rustプログラミング言語の13章練習問題をやってみた

技術,プログラミング言語,練習問題

TL;DR

プログラミング言語Rustの13章練習問題をやってみました。Rust初心者なので回答内容は保証しませんが、参考まで。

第13章

10章同様、練習問題とは銘打ってはいませんが、13.1のクロージャの説明内、「Cacher実装の限界」の項で2の問題を挙げてますので、そちらについて回答例を考えてみました。

いつものおまけですが、12章のminigrepのテストについて、深まった謎について。

問題点1.ハッシュマップを保持する

与えた引数をキーとして、そのキー毎に値を保持する仕組みを実装しなさいと解釈しました。

use std::thread;
use std::time::Duration;
use std::collections::HashMap;

#[derive(Debug)]
struct Cacher<T> where T: Fn(u32) -> u32 {
    calculation: T,
    values: HashMap<u32, u32>,
}

impl<T> Cacher<T> where T: Fn(u32) -> u32 {
    fn new(calculation: T) -> Cacher<T> {
        let values: HashMap<u32, u32> = HashMap::new();
        Cacher {
            calculation,
            values,
        }
    }

    fn value(&mut self, arg: u32) -> u32 {
        match (self.values).get(&arg) {
            Some(v) => *v,
            None => {
                (self.values).insert(arg, (self.calculation)(arg));
                *(self.values).get(&arg).unwrap()
            },
        }
    }
}

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));
        }
    }
}

fn main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(
        simulated_user_specified_value,
        simulated_random_number
    );
}

#[test]
fn call_with_difference_values() {
    let mut c = Cacher::new(|a| a);

    let v1 = c.value(1);
    let v2 = c.value(2);

    assert_eq!(v1, 1);
    assert_eq!(v2, 2);
}

まずはCache構造体を変更しないと話になりません。8行目のvalueをHashMap型にした上で、複数形にしてvaluesにしました。

同様に値を取り出すvalueメソッドも修正。こちらはmatch文はそのままで、Noneの時はcalculationのクロージャーを呼び出して、それをunwrap()してから返してます。matchで指定するのがgetの返り値で参照なので、参照外しが必要。

問題点2.u32以外も扱える様にする

オリジナルはu32を受け取ってu32を返すクロージャでしたが、そこをそれぞれ任意の型にしたいということと解釈しました。

いきなりやってもよかったのですが、まずは順を追うことにして、引数と返り値が同じ型として実装しました。その修正は特に載せません。それが動くのを確認してから、引数と返り値を別の型にできるように修正しました。

use std::thread;
use std::time::Duration;
use std::collections::HashMap;
use std::hash::Hash;

#[derive(Debug)]
struct Cacher<CT, AT, RT>
    where CT: Fn(AT) -> RT, AT: Eq + Hash + Copy, RT: Copy
{
    calculation: CT,
    values: HashMap<AT, RT>,
}

impl<CT, AT, RT> Cacher<CT, AT, RT>
    where CT: Fn(AT) -> RT, AT: Eq + Hash + Copy, RT: Copy
{
    fn new(calculation: CT) -> Cacher<CT, AT, RT> {
        let values: HashMap<AT, RT> = HashMap::new();
        Cacher {
            calculation,
            values,
        }
    }

    fn value(&mut self, arg: AT) -> RT {
        match (self.values).get(&arg) {
            Some(v) => *v,
            None => {
                (self.values).insert(arg, (self.calculation)(arg));
                *(self.values).get(&arg).unwrap()
            },
        }
    }
}

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));
        }
    }
}

fn main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(
        simulated_user_specified_value,
        simulated_random_number
    );
}

#[test]
fn call_with_difference_values_u32() {
    let mut c = Cacher::new(|a: u32| -> u32 { a });

    let v1 = c.value(1);
    let v2 = c.value(2);

    assert_eq!(v1, 1);
    assert_eq!(v2, 2);
}

#[test]
fn call_with_difference_values_i32() {
    let mut c = Cacher::new(|a: i32| -> i32 { a });

    let v1 = c.value(1);
    let v2 = c.value(2);

    assert_eq!(v1, 1);
    assert_eq!(v2, 2);
}

#[test]
fn call_with_difference_values_char() {
    let mut c = Cacher::new(|a: char| -> char { a });

    let v1 = c.value('a');
    let v2 = c.value('b');

    assert_eq!(v1, 'a');
    assert_eq!(v2, 'b');
}

#[test]
fn call_with_difference_values_usize_by_str() {
    let mut c = Cacher::new(|a: &str| -> usize { a.len() });

    let v1 = c.value("abc");
    let v2 = c.value("vwxyz");

    assert_eq!(v1, 3);
    assert_eq!(v2, 5);
}

都合、3つのジェネリックが出てくるのでパッと見では色々複雑。元々はCacherでクロージャ引数と返り値の型であるTだけでしたが、これをCT、AT、RTにしてみました。それぞれ、Closure Type、Argument Type、Return Typeの略です。ソースは先の問題点1の修正版をベースにしています。間に一段置いて動かしてから次に進んだので、あまり混乱せずにスムーズに修正できたかと思います。

where句のトレイトについてはコンパイラの警告を見て書き足しています。

コマンドライン引数を処理する関数のテストはどうする?

TRPL 13.2のイテレータの学習で、12章で書いたminigrepの処理をイテレータに書き換える話があります。このうち、Config::new()の引数をstd::env::Argsに書き換える部分があります。これ自体は特に問題ないのですが、問題はテストです。12章でこのConfig::new()に対するテストを書いています。が、引数が変わってしまったのでテスト関数も直さないとコンパイルが通りません。しかし、そこが問題なのです。

new()では1番目の引数としてコマンドライン引数をまとめた&[String]を受け取っています。これをstd::env::Argsに置き換えたわけです。テストの方はコマンドライン引数にしたい文字列の配列を作り、それをVec<String>に変換したものを渡しています。new()の引数を変えたので、その部分を変えればよいのですが、std::env::Argsにnew()はありません。これではConfig::new()をテストできません。皆さん、どうしているのでしょう??

Config::new()の引数をジェネリックにしてみる

Configにとってはイテレータを回せれば、その中身はenv::Argsである必要はないはず。そこで、コマンドライン引数を受ける引数argsを、Stringを持つIteratorトレイトを持つジェネリック型にしてみました。

impl Config {
    pub fn new<T>(mut args: T) -> Result<Config, &'static str>
        Iterator<Item = String>
    {
        // ...
    }
}

3行目の「Iterator<Item = String>」という書き方はTRPLの既読分には出てこない記述法かと思いますが、Iteratorだけでコンパイルしたところ、コンパイラがこう書くんだ!と教えてくれました。ここが通るようになったので、あとはテストの方です。

Config::new()のテストは正常パターン2種、異常パターン2種を用意していますが、こちらを以下の要領で書き換えました。

    #[test]
    fn config_ok2() {
        let args = [
            String::from("minigrep"),
            String::from("aaa"),
            String::from("bbb"),
            String::from("ccc"),
        ];
        let config = Config::new(args.into_iter()).unwrap();
        assert_eq!(config.query, "aaa");
        assert_eq!(config.filename, "bbb");
    }

Stringの配列をinto_iter()でイテレータにしてConfig::newに渡しています。気になるのは元々はassert_eq!ではargs[1]やargs[2]と比較していましたが、into_inter()でムーブしてしまうため使えなくなり、仕方なくハードコードしてます。まとめるなら、以下のようにまとめるのでしょうか?

    #[test]
    fn config_ok1() {
        let argument1 = "aaa";
        let argument2 = "bbb";
        let args = [
            String::from("minigrep"),
            String::from(argument1),
            String::from(argument2),
        ];
        let config = Config::new(args.into_iter()).unwrap();
        assert_eq!(config.query, argument1);
        assert_eq!(config.filename, argument2);
    }