Tambourine作業メモ

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

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

どうやらほとんど11章まで終わっていたみたいなので、12章に入る。

やりたいことはドルとフランの間で透過的に演算が出来ることだ。 「$5 + 10 CHF = $10 (レートが2:1の場合)」みたいなことがしたい。

ただし、まずやるべきは同じ通貨同士の足し算だ。

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

#[test]
fn test_simple_addition() {
    let sum = Money::dollar(5).add(Money::dollar(5));
    assert_eq!(Money::dollar(10), sum);
}

本ではメソッド名はplusになっているが、addにした。 というのも、Rustには演算子オーバーロードの機能があり、addを実装しておけば+で足し算が出来るようになりそうだから。 それも試してみよう。

もちろんまだテストは実行できない。メソッド定義してないし。

   Compiling money v0.1.0 (/Users/tambara/study/tdd_rust/money)
error[E0599]: no method named `add` found for struct `Money` in the current scope
  --> src/main.rs:75:36
   |
5  | pub struct Money {
   | ---------------- method `add` not found for this
...
75 |         let sum = Money::dollar(5).add(Money::dollar(5));
   |                                    ^^^ method not found in `Money`
   |
   = help: items from traits can only be used if the trait is implemented and in scope
   = note: the following trait defines an item `add`, perhaps you need to implement it:
           candidate #1: `std::ops::Add`

ただ、「お前、もしやstd::ops::Addトレイトが実装したいのでは?」と 言ってきてる。エスパーかな? このトレイトを実装すると、+が使えるようになるはずなワケ。

とりあえず、そこは置いておいて、テストを通すことにする。

pub fn add(&self, other: &Money) -> Money {
    Money {
        amount: self.amount + other.amount,
        currency: self.currency,
    }
}

酷いコードだけど、まずはこれでOK。

書いてみてわかったのは、引数には参照を貰うべきだね。 なので、テストも参照を渡すように変更。

let sum = Money::dollar(5).add(&Money::dollar(5));

これで問題ない。

さて、本ではこの後、通貨の換算をするのにImposterパターンを使うと言い出した。 恥ずかしながら初めて聞いた。ググってみたが、みんなあんまりよく知らない感じがする(笑)。

説明も読んでみたんだけど、ExpressionインターフェースとBankクラスがどういう働きをするのか、 12章の段階ではよくわからない。 Expressionインターフェースにはまだ何のメソッドもないし、Bankクラスはデータを持たずに ただreduceというメソッドを持つだけだ。

そこで、Expressionトレイトとbankモジュールという形で実装してみることにしよう。

まず、Java版のテストはこんな感じだ。

@Test
public void testSimpleAddition() {
    Money five = Money.dollar(5);
    Expression sum = five.plus(five);
    Bank bank = new Bank();
    Money reduced = bank.reduce(sum, "USD");
    assertEquals(Money.dollar(10), reduced);
}

素直にRustにしてみよう。

#[test]
fn test_simple_addition() {
    let five = Money::dollar(5);
    let sum: Expression = five.add(&five);
    let reduced: Money = bank::reduce(sum, "USD");
    assert_eq!(Money::dollar(10), reduced);
}

これはダメだ。Expressionはトレイトなのでsumのサイズがコンパイル時に決まらない。 参照にしてみよう。すると、エラーはなくなる。

しかし、addメソッド側で困ってしまう。

pub fn add(&self, other: &Money) -> &Expression {
    let m = Money {
        amount: self.amount + other.amount,
        currency: self.currency,
    };
    &m
}

これはエラーになる。

error[E0515]: cannot return reference to local variable `m`
  --> src/main.rs:49:9
   |
49 |         &m
   |         ^^ returns a reference to data owned by the current function

メソッドの中で作ったものを外に持ち出すのなら、移動させなければならない。 参照を返しても、参照が指しているオブジェクトを移動させないのならば、 メソッドが終わったらそのオブジェクトは無くなってしまう。

これをどうするのがRustらしいのかというのはあんまりよくわからないんだけど、 C言語的な発想ではmallocすることになるわけだ。

Rustではそういう場合、Boxという賢いポインタを使うことになる。 こんな良くないコードになるのはそもそもRustにマッチしていない設計を 選んでしまっているからじゃないかという疑いもあるけど。

pub fn add(&self, other: &Money) -> Box<Expression> {
    Box::new(Money {
        amount: self.amount + other.amount,
        currency: self.currency,
    })
}

このように作ったMoneyオブジェクトを、Boxに詰めて送り出す。 受ける方は、これでOK。

let sum: Box<Expression> = five.add(&five);

とりあえず、12章はこれで終わり。

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

9章に入る。いよいよ、通貨の導入だ。

その前に、Dollar構造体とFranc構造体の統一を片付けてしまおう。 9章の展開とは違うんだけど、とりあえず、以下の様なテストが通ることを目標にする。

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

    #[test]
    fn test_multiplication() {
        let five_d = Money::dollar(5);
        assert_eq!(Money::dollar(10), five_d.times(2));
        assert_eq!(Money::dollar(15), five_d.times(3));

        let five_f = Money::franc(7);
        assert_eq!(Money::franc(35), five_f.times(5));
        assert_eq!(Money::franc(77), five_f.times(11));
    }

    #[test]
    fn test_equality() {
        assert_eq!(Money::dollar(5), Money::dollar(5));
        assert_ne!(Money::dollar(5), Money::dollar(6));
        assert_eq!(Money::franc(7), Money::franc(7));
        assert_ne!(Money::franc(7), Money::franc(6));

        // 通貨が違うので、一致しない。
        assert_ne!(Money::franc(5), Money::dollar(5));
    }
}

ここにはDollarもFrancも影も形もない。

コードは今はこんな感じ。

trait Money {
    fn new(i: i32) -> Self;

    // Factory Method
    fn dollar(i: i32) -> Dollar {
        Dollar::new(i)
    }
    fn franc(i: i32) -> Franc {
        Franc::new(i)
    }

    fn amount(&self) -> i32;
    fn times(&self, multiplier: i32) -> Self
    where
        Self: Sized,
    {
        Self::new(self.amount() * multiplier)
    }
}

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

impl Money for Dollar {
    fn new(i: i32) -> Dollar {
        Dollar { amount: i }
    }

    fn amount(&self) -> i32 {
        self.amount
    }
}

impl PartialEq<Franc> for Dollar {
    fn eq(&self, _other: &Franc) -> bool {
        false
    }
}

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

impl Money for Franc {
    fn new(i: i32) -> Franc {
        Franc { amount: i }
    }
    fn amount(&self) -> i32 {
        self.amount
    }
}

impl PartialEq<Dollar> for Franc {
    fn eq(&self, _other: &Dollar) -> bool {
        false
    }
}

もちろんコンパイルエラーになるはずだ。まずは、エラーの解消を目指そう。

DollarとFrancにある処理をMoney構造体に集める。 ほぼ機械的にできるし、とてもコードが短くなった。

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

impl Money {
    fn dollar(i: i32) -> Money {
        Money { amount: i }
    }

    fn franc(i: i32) -> Money {
        Money { amount: i }
    }

    fn amount(&self) -> i32 {
        self.amount
    }

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

テストを実行すると、ドルとフランを比較するところでテストに失敗する。 以前は型が違う場合はエラーにしていたんだから、それはそうだ。

というわけで、いよいよここで通貨の導入だ。 本と同じように、文字列で通貨を表現することにしてみよう。

pub struct Money {
    amount: i32,
    currency: str,
}

しかし、この変更はコンパイルエラーになる。 strはいわゆるC言語の文字列みたいなものなので、サイズが決まらないからだ。

error[E0277]: the size for values of type `str` cannot be known at compilation time
  --> src/main.rs:11:26
   |
11 |     fn dollar(i: i32) -> Money {
   |                          ^^^^^ doesn't have a size known at compile-time
   |
   = help: within `Money`, the trait `std::marker::Sized` is not implemented for `str`
   = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
   = note: required because it appears within the type `Money`
   = note: the return type of a function must have a statically known size

そもそも、C言語に倣うならMoneyが持っているのは文字列へのポインタになるはずだ。 というわけで、strへの参照に変えてみる。

pub struct Money {
    amount: i32,
    currency: &str,
}

しかし、これもダメだ。

error[E0106]: missing lifetime specifier
 --> src/main.rs:7:15
  |
7 |     currency: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
5 | pub struct Money<'lifetime> {
6 |     amount: i32,
7 |     currency: &'lifetime str,
  |

currencyが指している先の文字列がずっとある保証がないからだ。 エラーメッセージでは、ライフタイムパラメタを入れたらどうかと言われている。

入れてみよう。

#[derive(Debug, Eq, PartialEq)]
pub struct Money {
    amount: i32,
    currency: &'static str,
}

impl Money {
    fn dollar(i: i32) -> Money {
        Money {
            amount: i,
            currency: "USD",
        }
    }

    fn franc(i: i32) -> Money {
        Money {
            amount: i,
            currency: "CHF",
        }
    }

    fn amount(&self) -> i32 {
        self.amount
    }

    fn times(&self, multiplier: i32) -> Money {
        Money {
            amount: self.amount() * multiplier,
            currency: self.currency,
        }
    }
}

'statucは、プログラムの起動中はずっと存在することを示すライフタイムだ。 このコードではcurrencyに入る可能性があるのは、文字列リテラルだけなのでこれでOK。

では、eqメソッドを実装しよう。ついでに、全部のメソッドにpubを付ける。 付けていないと、「使われていないよ」というワーニングが出るのがありがたい。

pub fn eq(&self, other: &Money) -> bool {
      if self.currency != other.currency {
          false
      } else {
          self.amount == other.amount
      }
}

これでテストも全部通過した。素晴らしい。

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

第8章に入る。

この辺りから、本のコードと自分のコードの差が大きくなって、 本の戦略がそのまま使えない感じになってきた。ちょっと試行錯誤しながら進めたい。

まず、本の方ではtimesメソッドがDollarとFrancの両方にあるので、それをまとめたいと言っている。 ところが、それはもうやっちゃった(笑)。 この先の行く末としてはサブクラスをなくして、Moneyに集約していきたいのかなと思っている。

本の方では、MoneyにDollarやFrancの生成の機能を持たせようとしていて、 テストコードで

Dollar five = new Dollar(5);

としているところを、

Money five = Money.dollar(5);

とFactory Methodで生成するようにして、テストコードにあらわにDollarクラスが出てこないようにしようとしている。

意図はわかる。こうしていって、テストコードにDollarが出てこないようにしておいて、しれっとDollarクラスを消しても グリーンが維持されれば理想的だ。Factory Methodはこんな感じ。

class Money {
    static Dollar dollar(int amount) {
        return new Dollar(amount);
    }
}

同じ事をRustの方でもできるのか。Moneyはトレイトだが実装は持てる。 Moneyトレイトのメソッドが、Dollarを返すことをは許されるのか。

trait Money {
    fn new(i: i32) -> Self;

    // Factory Method
    fn dollar(i: i32) -> Dollar {
        Dollar::new(i)
    }
    fn franc(i: i32) -> Franc {
        Franc::new(i)
    }

これは別にエラーにならない。へー。

ところが、テストコード側はエラーになる。

例えば、このようにMoney::francメソッドを呼び出すと、

#[test]
fn test_franc_multiplication() {
    let five = Money::franc(5);
    assert_eq!(Franc::new(10), five.times(2));
    assert_eq!(Franc::new(15), five.times(3));
}

このように、型アノーテーションが必要だと怒られる。

error[E0283]: type annotations needed
  --> src/main.rs:71:20
   |
11 |     fn franc(i: i32) -> Franc {
   |     ------------------------- required by `Money::franc`
...
71 |         let five = Money::franc(5);
   |                    ^^^^^^^^^^^^ cannot infer type
   |
   = note: cannot resolve `_: Money`

こうするのは問題にならない。が、そういうことじゃない(笑)。

#[test]
fn test_franc_multiplication() {
    let five = Franc::franc(5);
    assert_eq!(Franc::new(10), five.times(2));
    assert_eq!(Franc::new(15), five.times(3));
}

これはつまり、このタイミングでMoneyをトレイトから構造体に格上げしてやる必要があると言うことなんだろう。

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

7章に入る。

ドルとフランは比較できるのだろうかという疑問をテストに書いてみるところから始まる。

この本のコードでは、equalsメソッドはObjectを受けているので、ドルとフランは区別なく 扱われ、スイス経済に大打撃を与えている。

一方、私のRustの実装はEqトレイトに任せているので、同じ型しか比較できない。 テストはコンパイルエラーとなる。 それはマズいので、違う通貨の場合には必ずfalseになるコードにしよう。

・・・と考えて、いろいろ試行錯誤してみたのだが、そもそもRustではJavaのように どんなものでも渡ってくるようなメソッドが書けない。 書けなくはないんだけど、動的に渡ってきた引数に対してチェックする事が出来ない。

具体的には、

impl<T> PartialEq<T> for Dollar {
    fn eq(&self, _other: &T) -> bool {
        false
    }
}

として、Tの型によって動作を変えるようなことがしたいんだけど、 どうしていいかわからない。

Rustの定石としては、enumが渡ってくるとして パターンマッチで動きを変える事になると思う。

いったんは、

impl PartialEq<Franc> for Dollar {
    fn eq(&self, _other: &Franc) -> bool {
        false
    }
}

これをDollarとFrancの両方に実装して、テストは通してしまおう・・・。 それにしても、そもそもDollarとFrancはほとんど同じなので無くしてしまいたいんだよな・・・と 本の方でも言い始めている。

ともあれ、これで7章はクリア。

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

5章「原則をあえて破るとき」に入る。

この章は、クソコードを作って終わり。 ただし、これをどうにかしないうちは家に帰らないことも誓う。

とりあえず、今まで作ったドルとは別にフランを作る。それも単なるコピペで作る。 フラン・・・そんな通貨単位はもはや存在しない。時代を感じる。 あれ?CHFはスイスフランか。まだあるわ。

作ったテストは

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

これを充たすFranc構造体をDollarをコピペして5章は終わり。6章に入ろう。

6章では、このコピペを解消する。 Javaなので、DollarクラスとFrancクラスの親クラスとして Moneyクラスを導入する。

さて、Rustではどうするのだろうか。Moneyトレイトを作ることになる・・・んだと思う。 やってみよう。

Javaオブジェクト指向言語と違って、Rustでは継承がない。 トレイトによってメソッドの実装を引き継がせることは出来るのだが、 フィールドの定義を共有することはできない。 通常、構造体のデータを共有したいのなら、最も単純かつ適切な手段はHas-a、 つまり共有部分を別の構造体にして、それを構造体の要素とすることだろう。 というわけで、要素が1つしかない今現在はそれもあまり意味が無い。

この時点でベストだと考えられるのは、Money構造体を作ってそこから タプル構造体で新しい型としてDollarとFrancを作ることかも知れない。 しかし、それも先の展開を考えるとイマイチな気がする・・・。 とりあえず、timesメソッドの共有を考えてみよう。

Dollarのtimesの実装はこうなっている。

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

つまり、Dollerのインスタンスを作ることと、 インスタンスからamountの値を得ることができれば実装できる。

というわけで、Moneyトレイトを作ってみた。

trait Money {
    fn new(i: i32) -> Self;
    fn amount(&self) -> i32;
    fn times(&self, multiplier: i32) -> Self
    {
        Self::new(self.amount() * multiplier)
    }
}

これはコンパイルエラーになる。

the size for values of type `Self` cannot be known at compilation time

doesn't have a size known at compile-time

help: the trait `std::marker::Sized` is not implemented for `Self`
note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
note: the return type of a function must have a statically known size
help: consider further restricting `Self`: ` where Self: std::marker::Sized `

Selfを返すモノを実装してるけど、この関数はコンパイル時にSelfのサイズが 決まらない恐れがあるのでダメだと言っている。 多分、Vecのようなホントに大きさが決まらないモノだとダメなんだろう。 メッセージの最後にSelfをSizedだと制限すれば良いんだと書いてある。なるほど?

こうかな?

trait Money {
    fn new(i: i32) -> Self;
    fn amount(&self) -> i32;
    fn times(&self, multiplier: i32) -> Self
    where
        Self: Sized,
    {
        Self::new(self.amount() * multiplier)
    }
}

おお、いけた。

というわけで、timesの実装は書かなくて良くなったものの、 amountというメソッドを新たに実装する必要が出来た。

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

impl Money for Dollar {
    fn new(i: i32) -> Dollar {
        Dollar { amount: i }
    }

    fn amount(&self) -> i32 {
        self.amount
    }
}

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

impl Money for Franc {
    fn new(i: i32) -> Franc {
        Franc { amount: i }
    }
    fn amount(&self) -> i32 {
        self.amount
    }
}

かなりの重複感であり、まだ大分クソコードな感じが強い。イケてない。 ただ、これ以上の対応はもう少し先の実装を進めてからにしてみたい。

というわけで、6章まで終わり。

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

4章「意図を語るテスト」に入っていこう。

amountという内部構造がインターフェースとして外に出ているのは 見づらいし良くないよねということでリファクタリングしている。 もっともだ。 そもそも、こちらにはコンストラクタすらないので、このタイミングで作ることにしよう。

impl Dollar {
    pub fn new(i: i32) -> Dollar {
        Dollar { amount: i }
    }
}

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

コンストラクタの名前は慣習的にnewがよく使われるけど、なんでも構わない。

というわけで、テストからamountというフィールド名が消えたので満足。4章は短い。

ケント・ベックの「テスト駆動開発」の写経を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 });
    }
}