Tambourine作業メモ

主にスキル習得のためにやった作業のメモ。他人には基本的に無用のものです。

ケント・ベックの「テスト駆動開発」の写経をRustでやってみる(16)

15章。ついに「5ドル+10フラン」のテストを書くぞと、ケント・ベック先生は意気込んでいる。こんなテストを書いている。

@Test
public void testMixedAddition() {
    Expression fiveBucks = Money.dollar(5);
    Expression tenFrancs = Money.franc(10);
    Bank bank = new Bank();
    bank.addRate("CHF", "USD", 2);
    Money result = bank.reduce(fiveBucks.plus(tenFrancs), "USD");
    assertEquals(Money.dollar(10), result);
}

冒頭に上げられているテストを素直にRustにすると、こんな感じだ。これまで作ってきたものが一堂に会した。

#[test]
fn test_mixed_addition() {
    let five_bucks = Money::dollar(5.);
    let ten_francs = Money::franc(10.);
    let bank = Bank::new();
    bank.add_rate(CHF, USD, 2.0);
    let result = bank.reduce(five_bucks.add(ten_francs), USD);
    assert_eq!(Money::dollar(10.), result);
}

書いている私としても、特に違和感がない。動いてしかるべきという感じがするんだけど、コンパイルエラーになる。出ているエラーは3つ。まず、1つめ。

error[E0308]: mismatched types
   --> src/main.rs:248:49
    |
248 |         let result = bank.reduce(five_bucks.add(ten_francs), USD);
    |                                                 ^^^^^^^^^^
    |                                                 |
    |                                                 expected `&Money`, found struct `Money`
    |                                                 help: consider borrowing here: `&ten_francs`

addに渡すのは参照でなければならない。そうだった。そう作った。 ただ、Moneyは本質的にValue ObjectなのでCopyトレイトを実装して実体渡しが出来るようにしてもいいのかもしれない。

2つめ。上の実体渡し案は、こっちをみるとイマイチに感じる。

error[E0308]: mismatched types
   --> src/main.rs:248:34
    |
248 |         let result = bank.reduce(five_bucks.add(ten_francs), USD);
    |                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^
    |                                  |
    |                                  expected reference, found struct `Sum`
    |                                  help: consider borrowing here: `&five_bucks.add(ten_francs)`
    |
    = note: expected reference `&_`
                  found struct `Sum`

同じようにback.reduceにも参照を渡さなければならない。しかも、こっちはExpressionの参照が期待されているので実体は渡せない(コンパイル時にサイズが決まらないから)。だとしたら、addの参照渡しでいいか・・・。この辺りは何が普通なのか、Rustの経験が不足していてわからない。

3つめ。これも自分で入れた変更を忘れている。

error[E0308]: mismatched types
   --> src/main.rs:249:9
    |
249 |         assert_eq!(Money::dollar(10.), result);
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `Money`, found enum `std::result::Result`
    |
    = note: expected struct `Money`
                 found enum `std::result::Result<Money, &str>`
    = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)

そうだ。いまや、reduceはResultを返すのだった。

というわけで、テストを直した。

#[test]
fn test_mixed_addition() {
    let five_bucks = Money::dollar(5.);
    let ten_francs = Money::franc(10.);
    let bank = Bank::new();
    bank.add_rate(CHF, USD, 2.0);
    let result = bank.reduce(&five_bucks.add(&ten_francs), USD);
    assert_eq!(Money::dollar(10.), result.unwrap());
}

どうだっ!

error[E0596]: cannot borrow `bank` as mutable, as it is not declared as mutable
   --> src/main.rs:247:9
    |
246 |         let bank = Bank::new();
    |             ---- help: consider changing this to be mutable: `mut bank`
247 |         bank.add_rate(CHF, USD, 2.0);
    |         ^^^^ cannot borrow as mutable

はいはい、はいはい。bankはmutじゃないとダメだった。しかし、一度、レートをセットしたあとは、immutableなbankを使いたい感じはある。どうしたら・・・?

それは後で考えることにして、let mut bankのようにmutableな変数を取ることにする。これでコンパイルは通った。

---- tests::test_mixed_addition stdout ----
thread 'tests::test_mixed_addition' panicked at 'assertion failed: `(left == right)`
  left: `Money { amount: 10.0, currency: USD }`,
 right: `Money { amount: 15.0, currency: USD }`', src/main.rs:249:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

テストは失敗。sumをちゃんと実装していないので当然だ。今はこうなってる。

impl Expression for Sum {
    fn reduce(&self, to: Currency, _: &Bank) -> Result<Money, &'static str> {
        Ok(Money {
            amount: self.augend.amount + self.addend.amount,
            currency: to,
        })
    }
}

augendとaddendの両方をあらかじめ換算しておく必要がある。こんなイメージだ。

fn reduce(&self, to: Currency, bank: &Bank) -> Result<Money, &'static str> {
    Ok(Money {
        amount: self.augend.reduce(to, bank).amount + self.addend.reduce(to, bank).amount,
        currency: to,
    })
}

考え方はこれで良いのだけれど、Money.reduceはMoneyではなく、Resultを返すので、エラーになる。

error[E0609]: no field `amount` on type `std::result::Result<Money, &'static str>`
  --> src/main.rs:94:50
   |
94 |             amount: self.augend.reduce(to, bank).amount + self.addend.reduce(to, bank).amount,
   |                                                  ^^^^^^

ここで、Rustのお素敵機能として、Resultを返す関数の中で呼び出した関数がResultを返す時(ややこしいけど、ついてきてる?)、呼び出した関数がErrの方を返したときにはそのままそのErrを関数の呼び出し値としてリターンするという機能がある。

日本語で説明するのは無理っぽい。

えーっと、つまり、丁寧にやるならば、さっきのコードはパターンマッチを使ってResultを場合分けして処理しなくてはいけない。こんな感じだ。

fn reduce(&self, to: Currency, bank: &Bank) -> Result<Money, &'static str> {
    let augend_reduce = match self.augend.reduce(to, bank) {
        Ok(m) => m,
        Err(e) => return Err(e),
    };

    let addend_reduce = match self.addend.reduce(to, bank) {
        Ok(m) => m,
        Err(e) => return Err(e),
    };

    Ok(Money {
        amount: augend_reduce.amount + addend_reduce.amount,
        currency: to,
    })
}

だいぶまだるっこしい。しかし、?演算子を使うと、上と同じコードを以下のように書ける。

fn reduce(&self, to: Currency, bank: &Bank) -> Result<Money, &'static str> {
    Ok(Money {
        amount: self.augend.reduce(to, bank)?.amount + self.addend.reduce(to, bank)?.amount,
        currency: to,
    })
}

超便利。これはナイスになって蘇った(死んでないか)Javaの検査例外のように見える。

これでテストはバッチリ通った。

ただし、元の本では、15章はもうちょっと続く。