Tambourine作業メモ

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

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

3章「三角測量」に入る。

DollarをValue Objectにしたいと言い出す。等値であることをチェックできるようにするのだと。

Value Objectというパターンに馴染みはないんだけど、 オブジェクトを値として扱うことにはいろいろな難しさがあることは知っている。 そもそも、Javaにプリミティブ型などという非純粋なものがあるのもそこに起因するし、 等値であることを==ではなくequalsでチェックすることになるなどの ドジの原因もそうだ。

そもそも、等値は難しい。JavaScriptは「==ではなくて===を使いなさい」などと 初学者に「こいつに付いていて大丈夫だろうか」と思わせるようなことを言うし、 Rubyは、「値を比較するには、eql?/equal?/==/===の4種類のメソッドがあります」などと言い出す。

もちろん、Rustの等値も難しい。Eqトレイトが絡んでくるアレだ。 が、それは後で考えることにして、バカみたいにJavaを倣って進んでいくことにする。 というわけで、ここはあえてequalsメソッドをDollar構造体に実装することにする。

追加したテストと仮実装はこんな感じだ(mod testsは省略しているが、 実際はtestsモジュールにテストは書いている)

impl Dollar {
    pub fn equals(&self, dollar: &Dollar) -> bool {
        false
    }
}

#[test]
fn test_equality() {
    assert!(Dollar { amount: 5 }.equals(&Dollar { amount: 5 }));
}

構造体式に参照演算子&を付けたり、equalsメソッドを.で呼び出したりするとき 演算子の優先順位がどうなるのか、つまりカッコでどこかをくくらなければいけないのか ちょっと不安になったけど、そのまま試しに書いてみたら特にシンタックスエラーには ならなかった。

さあ、テストを実行してみよう。

running 2 tests
test tests::test_multiplication ... ok
test tests::test_equality ... FAILED

failures:

---- tests::test_equality stdout ----
thread 'tests::test_equality' panicked at 'assertion failed: Dollar{amount: 5,}.equals(&Dollar{amount: 5,})', src/main.rs:33:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::test_equality

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

もちろん、equalsが常にtrueを返す様に実装すれば、このテストは通る。

この本では、常に通る仮実装を作って、それでは通らないようにするために falseが必要になるテストを追加する・・・と進んでいるが、 実際にコードを書くときには、書いたテストが機能しているのか確かめるために わざと絶対通る様にしたり、絶対失敗するようにして確かめることをよく行う。大事だ。 trueとfalseを両方書いてみて、テストコードがこのメソッドを呼び出していることに 確信が持てた。自信を得ることこそユニットテストの目的だから、 自信が持てていないことは確かめる必要があるし、テストがちゃんと機能しているかどうかは 一番大事なことだから、ちゃんと自信を持つべきだ。

さて、では二つ目のケースをテストに足してテストが失敗することを確認したら、 ちゃんとした実装を目指そう。といっても、何も難しくない。 基本的には、すべての要素を比較すれば良いだけだし、そもそも1つの要素しかない。

pub struct Dollar {
    pub amount: i32,
}

impl Dollar {
    pub fn times(&self, multiplier: i32) -> Dollar {
        Dollar {
            amount: self.amount * multiplier,
        }
    }

    pub fn equals(&self, dollar: &Dollar) -> bool {
        self.amount == dollar.amount
    }
}

そして、何も難しくないことはコンピューターにも出来る(これについては、後で述べる)。

今、比較のためのメソッドとしてequalsを作ったが、 Rustの流儀としては、==で比較できるべきだ。 assert_eq!マクロは、2つの引数を==で比較する。 それを使ってtest_equality()を書き直そう。

#[test]
fn test_equality() {
    assert_eq!(Dollar { amount: 5 }, Dollar { amount: 5 });
    assert_ne!(Dollar { amount: 5 }, Dollar { amount: 6 });
}

こう書き直すと、コンパイルエラーになる。

binary operation `==` cannot be applied to type `Dollar`

note: an implementation of `std::cmp::PartialEq` might be missing for `Dollar`

Dollarを==で比較することはできないよと1行目は言っている。 さらに、2行目では「std::cmp::PartialEqの実装を忘れてるんじゃない?」と言ってる。 本当に驚くべき親切さだ。で、PartialEqって何だろう。

その秘密はRustの奥深さの奥にあるので各自勝手に勉強することとして、 すごくざっくりと言えばeqメソッドとneメソッドが定義されたトレイトだ。 トレイトはJavaでいうインターフェース、Rubyでいうとモジュールみたいなものだ。 トレイトは実装を持てて、neはeqをnotしたものとあらかじめ実装されているので、 実装しなければならないのはeqだけだ。 ちなみに、==演算子とeqメソッドは表記が違うだけの同じモノである。

impl PartialEq for Dollar {
    fn eq(&self, other: &Dollar) -> bool {
        self.amount == other.amount
    }
}

やっていることはさっきのequalsメソッドの実装となんら変わらない。 ただし、これを書くだけで==と!=が使えるようになるはずだ。

しかし、なんでPartialなのか。それは浮動小数点の不思議さに由来しているらしい。 詳細は省くけど、浮動小数点じゃないならPartialじゃないEqも充たすらしい。 ただし、EqトレイトはPartialじゃないよというマーカーで、何も追加して実装する必要はない。 付けておくべきなので、付けておくことにしよう。

impl Eq for Dollar{}

さて、まだコンパイルエラーは消えてない。

`Dollar` doesn't implement `std::fmt::Debug`

`Dollar` cannot be formatted using `{:?}`

help: the trait `std::fmt::Debug` is not implemented for `Dollar`
note: add `#[derive(Debug)]` or manually implement `std::fmt::Debug`
note: required because of the requirements on the impl of `std::fmt::Debug` for `&Dollar`
note: required by `std::fmt::Debug::fmt`

Dollarにはstd::fmt::Debugが実装されていないよと言ってる。 assert_eq!マクロは、テストが失敗したときに両者の値を表示してくれる。 そのときにどうやって表示するかはDebugトレイトで指定をするのだ。

相変わらずの親切さによって「#[derive(Debug)]したら?」と教えてくれている。 deriveはトレイトの実装をお任せする機能だ。 要素に全部Debugが実装されているようなものであれば、 おそらくそれを全部表示するのが求められる実装だろうと考えて、 勝手に作ってくれる。便利だ。

#[derive(Debug)]
pub struct Dollar {
    pub amount: i32,
}

これでコンパイルエラーは消えた。テストを実行しよう。 ちゃんとテストが通った。

めでたしめでたし。

・・・まだ疑問はある。さっき、Debugトレイトはお任せ実装に委ねた。 PartialEqもやっていることは持っている全要素についてeqしただけだった。 Debugがお任せできるのなら、Eqもお任せできるのではないだろうか。

そう、できる。

というわけで、最終的なソースコードはこうなる。 言語の進化を感じるよな。

fn main() {}

#[derive(Debug, Eq, PartialEq)]
pub struct Dollar {
    pub amount: i32,
}

impl Dollar {
    pub fn times(&self, multiplier: i32) -> Dollar {
        Dollar {
            amount: self.amount * multiplier,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_multiplication() {
        let five = Dollar { amount: 5 };
        assert_eq!(10, five.times(2).amount);
        assert_eq!(15, five.times(3).amount);
    }

    #[test]
    fn test_equality() {
        assert_eq!(Dollar { amount: 5 }, Dollar { amount: 5 });
        assert_ne!(Dollar { amount: 5 }, Dollar { amount: 6 });
    }
}