ベックのTDD本のMoneyサンプルをプロパティベースでやってみた

プロパティベースのテストコーディングの練習として、ベックの TDD本を題材に Scala と ScalaCheck でやってみた記録。OOP と FP の感覚の違いについてもちょっと考えてみる。

##方針など

- 『Test Driven Development: By Example』[^tdd] をテキストにした
- テキストの中のテストコードはアサーションベースで書かれていて、いわゆる「**特殊解**[^special_case]」を並べたテストコードになっているが、ここでは「**一般解**」を意識して**プロパティ**として記述してみる。
- なるべくテキストに追従するが、必ずしも記載通りの手順をなぞってはいない。ただし**TDDのステップを細かく刻んでいくスタイル**は踏襲する。
- なるべく**プロパティを先に考える**ようにして、いわばプロパティファースト、プロパティ駆動のような段取りを意識する。
- 各段階のソースを gist に置いた。依存ライブラリは以下の感じ。
 - "org.typelevel" % "cats-core_2.12" % "1.0.0-MF"
 - "org.scalacheck" %% "scalacheck" % "1.13.4" % "test"

[^tdd]: ["Test Driven Development: By Example"(Kent Beck 2002/11)](http://www.informit.com/store/test-driven-development-by-example-9780321146533)
[^special_case]: プロダクトコードは「一般解」、テストコードは「特殊解」を扱うべしとするガイドライン。日本での JUnit普及期に[故石井勝氏が提唱](http://objectclub.jp/community/memorial/homepage3.nifty.com/masarl/article/junit/programming-style-guide.html#%E3%83%86%E3%82%B9%E3%83%88%E3%82%B3%E3%83%BC%E3%83%89%E3%81%A7%E4%B8%80%E8%88%AC%E8%A7%A3%E3%82%92%E6%89%B1%E3%82%8F%E3%81%AA%E3%81%84%E3%81%93%E3%81%A8)したもの。

##試行
###1. Multi-Currency Money
- Dollarクラス（のちの Money クラス）の叩き台。テキストにならって、あえてひとまずミュータブルに実装したが、後でイミュータブルな実装にリファクタする。

```scala
class Dollar(var amount: Int) {
  def times(multiplier: Int): Unit = amount = amount * multiplier
}
```

##### プロパティ
`times` はドルを整数倍するメソッドで、、、

- 0ドルを x倍したものは常に 0ドル (xは整数)
- 1ドルを x倍したものは常に xドル (xは整数)
- aドルの b倍と bドルの a倍は等しい (a, bは整数)
- aドルの b倍を c倍したものと、aドルを b × c倍したものは等しい (a, b, cは整数)


```scala
property("$0 * x = $0") = forAll { (x: Int) =>
  val zero: Dollar = new Dollar(0)
  zero.times(x)
  zero.amount == 0
}
property("$1 * x = $x") = forAll { (x: Int) =>
  val five: Dollar = new Dollar(1)
  five.times(x)
  five.amount == x
}
property("$a * b = $b * a") = forAll { (a: Int, b: Int) =>
  val d1: Dollar = new Dollar(a)
  d1.times(b)
  val d2: Dollar = new Dollar(b)
  d2.times(a)
  d1.amount == d2.amount
}
property("($a * b) * c = $a * (b * c)") = forAll { (a: Int, b: Int, c: Int) =>
  val d1: Dollar = new Dollar(a)
  d1.times(b)
  d1.times(c)
  val d2: Dollar = new Dollar(a)
  d2.times(b * c)
  d1.amount == d2.amount
}
```
Dollar がミュータブルなせいもあってか、だいぶモッサリしている。

[gist: ch01 mumti-currency money](https://gist.github.com/yasuabe/f1637d479e2c6bf5c2c99ce2ffd078eb)

###2. Degenerate Objects
プロパティは変えないまま`Dollar`をイミュータブルにし、テストコードも合わせて変更。乗算の関数は `*` をオーバーロードした。これ以降、プロダクトコードの変更過程には触れず、最終段階のみ最後に示すことにする。

```scala
case class Dollar(amount: Int) {
  def *(multiplier: Int): Dollar = Dollar(amount * multiplier)
}
```

プロパティの実装もだいぶスッキリした

```scala
property("$0 * x = $0") = forAll { (x: Int) =>
  Dollar(0) * x == Dollar(0)
}
property("$1 * x = $x") = forAll { (x: Int) =>
  Dollar(1) * x == Dollar(x)
}
property("$a * b = $b * a") = forAll { (a: Int, b: Int) =>
  Dollar(a) * b == Dollar(b) * a
}
property("($a * b) * c = $a * (b * c)") = forAll { (a: Int, b: Int, c: Int) =>
  (Dollar(a) * b) * c == Dollar(a) * (b * c)
}

```
[gist: ch02 degenerate objects](https://gist.github.com/yasuabe/664faa0b975c912e1e6fc9af07a9b8ec)

###3. Equality for All
equals 実装の章。

##### プロパティ

- aドル == bドル ⇔ a == b (ドル同士の等値性と金額を表す数の等値性は同値)
- aドル == bドル ⇔ (aドル equals bドル) (`==` と`equals`は同じ)
- `equals` の基本のプロパティ
 - 反射率：aドル == aドル
 - 対象律：aドル == bドル ⇔ bドル == aドル
 - 推移律：aドル == bドル ∧ bドル == cドル  ⇒ aドル == cドル

```scala
property("$a == $b <=> a == b") = forAll { (a: Int, b: Int) =>
  (Dollar(a) == Dollar(b)) == (a == b)
}
property("$a == $b <=> $a.equals($b)") = forAll(dollar2) { case (a, b) =>
  (a equals b) == (a == b)
}
property("reflexive law") = forAll(dollars) { (a) =>
  a equals a
}
property("symmetric law") = forAll(dollar2) { case (a, b) =>
  (a equals b) == (b equals a)
}
property("transitive law") = forAll( for {
  oneOf3 <- unique3.map { case (a, b, c)=> Gen.oneOf(a, b, c) }
  x      <- oneOf3
  y      <- oneOf3.suchThat(_ equals x)
  z      <- oneOf3.retryUntil(_ equals y)
} yield (x, z)) { case ((x, z)) => x equals z }
```
推移律だけ少しややこしい。ここでは異なる３つの数から、まず任意の $x$ を選び、次に $x$ と等しい $y$ を選び、さらに $y$ と等しい $z$ を選ぶと、全ての  $x$ ,  $z$ の組について等号が成立ことを書き下した。ヘルパ用のテストコードも若干書き足している。

[gist: ch03 equality for all](https://gist.github.com/yasuabe/091caadd648ceddf744dc8181895b734)

###4. Privacy
`amount` を privateとしたが、プロパティには影響なし。

以下のように`cats`を用いてテストコードを少しリファクタした。例えば下のような変更前コードに対して、

```scala
private def dollar2 = for {
  a <- dollars
  b <- dollars
} yield (a, b)
private def dollar3 = for {
  a <- dollars
  b <- dollars
  c <- dollars
} yield (a, b, c)
```

`cats`の`Apply`インスタンスを下のように定義しておけば、

```scala
implicit val genApply: Apply[Gen] = new Apply[Gen] {
  override def ap[A, B](gf: Gen[A => B])(ga: Gen[A]) = gf flatMap ga.map
  override def map[A, B](ga: Gen[A])(f: A => B) = ga map f
}
```

cats.syntax.apply._を用いて以下のように置き換えられ、dollarのGen以外にも使えるようになる。 

```scala
def double[G[_]: Apply, A](ga: G[A]): G[(A, A)]    = (ga, ga).mapN((_, _))
def triple[G[_]: Apply, A](ga: G[A]): G[(A, A, A)] = (ga, ga, ga).mapN((_, _, _))
```

[gist: ch04 privacy](https://gist.github.com/yasuabe/1feef267d4d1efefcffbbbda67205ff7)


###5. Franc-ly Speaking
プロダクトコードでは、ほぼ `Dollar` からのコピペで `Franc` を追加。重複コードはテキストの通りあとで取り除く。

プロパティとしては、「特殊解」 CHF * 2 = 10 CHFのみ記述。

```scala
property("5 CHF * 2 = 10 CHF") = Prop {
  Franc(5) * 2 == Franc(10)
}
```
プログラミングを前に進めるという意味では、`forAll`による一般的性質の記述ではなく、こんな感じで`Prop`で「特殊解」だけ書いてとりあえず先に進むのもありだと思う（あとで直す前提で）。

[ch5 franc-ly speaking](https://gist.github.com/yasuabe/6785292af9a41cf5141239a15880b99d)

###6. Equality for All, Redux
プロパティは変えないまま`Dollar`と`Franc`で共通の基底クラス`Money`クラスを作り、`equals`を引き上げた

[gist: ch06 equality for all, redux](https://gist.github.com/yasuabe/19259882dc93d8cc41c98aaaaf1ab39e)

###7. Apples and Oranges
この章では通貨が異なる場合を含む`equals`を実装。
まず「`amount`が等しい`Money`の等値性は、`Money`の型の等値性と同値」というプロパティを記述（getClass がまずいのはあとで直す）。

```scala
property("(m1 == m2) == (m1.getClass == m2.getClass)") =
    forAll(double(consGen), intGen) { case ((c1, c2), n) =>
  val m1 = c1(n)
  val m2 = c2(n)
  (m1 == m2) == (m1.getClass == m2.getClass)
}
```
テストヘルパとして、整数から任意の`Money`型のコンストラクタを生成する`Gen`なども書いた。

[gist: ch07 apples and oranges](https://gist.github.com/yasuabe/6785292af9a41cf5141239a15880b99d)

###8. Makin' Objects
この章ではプロパティの中で`Dollar`型固定で書いてた部分を、任意の通貨の`Money`を用いるようにして一般化した。このため 5章で書いた`Franc`のための特殊解テストも無用になり削除した。

```scala
  property("m(0) * x = m(0)") = forAll { (x: Int, m: Int => Money) =>
    m(0) * x == m(0)
  }
  property("m(1) * x = m(x)") = forAll { (x: Int, m: Int => Money) =>
    m(1) * x == m(x)
  }
  property("m(a) * b = m(b) * a") = forAll { (a: Int, b: Int, m: Int => Money) =>
    m(a) * b == m(b) * a
  }
  property("(m(a) * b) * c = m(a) * (b * c)") = forAll { (a: Int, b: Int, c: Int, m: Int => Money) =>
    (m(a) * b) * c == m(a) * (b * c)
  }
...など
```
任意の通貨の`Money`型のオブジェクトを生成する Genも書いた。

```scala
implicit val intGen: Gen[Int] = Gen.chooseNum(-1000000, 1000000)
implicit val consGen: Gen[Int => Money] = Gen.oneOf(Dollar(_:Int): Money, Franc(_:Int): Money)
implicit val moneyGen: Gen[Money] = (consGen, intGen).mapN(_(_))
```

[gist: ch08 makin' objects](https://gist.github.com/yasuabe/2e52a307624acfc350d6683b9b1b3ef8)

###9. Times We're Living in
通貨フィールドを追加する章. 等値性テストにも通貨を加味。

プロパティの中でも `getClass` など 'smelly'なコードを、`Money#currency` へのアクセスに替えた。

```scala
property("(m1==m2) == (m1.currency==m2.currency)") =
  forAll(double(currencyGen), intGen) { case ((c1, c2), n) =>
    val (m1, m2) = mapPair(c1, c2)(constructor(_)(n))
    (m1 == m2) == (m1.currency == m2.currency)
  }
```

[gist: ch09 times we're living in](https://gist.github.com/yasuabe/205ed36e8d384abdbe27f56621fcb981)

###10. Interesting Times - 11. the Root of All Evil
`Dollar`と`Franc`の重複要素を`Money`に引き上げて、不要になった`Dollar`/`Franc`を削除。プロパティはそのまま。(10章と11章をひとまとめに書いた.)

[gist: ch10 interesting times- ch11 the root of all evil](https://gist.github.com/yasuabe/99923da2ba33da9d1093c57cce842ec8)

###12. Addition Finally
加算の実装、Bankクラス追加、Expressionクラスのダミー実装の章

##### プロパティ
整数 $a$, $b$ をそれぞれ Money にしてから足し合わせた Money と、整数 $a + b$ を Money にしたものは等しい[^homomorphic]。

```scala
property("money(a + b) == money(a) + money(b)") =
  forAll(intGen, intGen, moneyConsGen) { (a, b, money) =>
     Money.dollar(10) == new Bank().reduce(money(a) + money(b), "USD")
  }
```
`Bank#reduce` (通貨換算メソッド)について、ここでは差し当たり常に10ドルを返すダミー実装とした。

[gist: ch12 addition finally](https://gist.github.com/yasuabe/69ac60df4320ebe4fa175006d7c1dea8)

###13. Make it
プロダクトコードでは `Bank` の `reduce` を `Expression` に移譲するように修正。

##### プロパティ
前章のダミーコードを直した上で、さらに一つ追加。

- 2数 `a`, `b` を足してから `Money` にしても、`Money` にしてから足しても同じ
- `Money` オブジェクトをそれ自身の通貨で `reduce` したものはそれ自身に等しい

```scala
property("m(a + b) == m(a) + m(b)") =
  forAll(double(intGen), moneyConsGen) { case ((a, b), m) =>
    m(a + b) equiv m(a) + m(b)
  }
property("m == reduce(m, m.currency)") =
  forAll(intGen, moneyConsGen) { (n, m) =>
    reduce(m(n)) == m(n)
  }
```

[gist: ch14 make it](https://gist.github.com/yasuabe/6e1cc0bb4ae9cbac1b0e355742b8b66b)

###14. Change
reduce 完成の章。

##### プロパティ
- 「c1 : c2 == r」 ならば 「reduce(c1(n)) == c2(n / r)」
 - 例) c1→フラン、c2→ドル、c1:c2=r=2、n=10 とすると、10フランをレート2:1でドル換算したものと(10/2)ドルは等しい
- 同じ通貨間の変換レートは1

```scala
property("reduce(c1(n)) == c2(n / rate)") =
  forAll(intGen, rateGen) { case (n, rate@((src, dst), r)) =>
    val (c1, c2) = mapPair(src, dst)(constructor)
    val reduce   = (e: Expression) => bank(rate).reduce(e, dst)

    reduce(c1(n)) == c2(n / r)
  }
property("identity rate") = forAll(currencyGen) { c: String =>
  new Bank().rate(c, c) == 1
}
```

[gist: ch14 change](https://gist.github.com/yasuabe/fe8406c5fc3866e2f0aefe2f2a19eff8)

###15. Mixed Concurrencies - 16. Abstraction Finally
15-16章では通貨が異なる場合をふくむ加算を実装（作業の都合上15章と16章をまとめた）。

##### プロパティ
- reduce(c1(n1) + c2(n2)) == c2(n1 / r + n2)
 - 例) フランとドルの比が2:1のとき、10フランと5ドルを足し合わせたものをドルに換算したものは、(10/2+5)ドルに等しい。(c1→フラン, c2→ドル, r→2, n1→10, n2→5)
- reduce(x + y) equiv reduce(x) + y
 -  $x$ が換算元のお金、 $y$ が換算先のお金の場合、 $x$ と $y$ を足してから $y$ の通貨にならしても、 $x$ を $y$ の通貨に合わせてから足しても同じ
- 結合則: (x + y) + z = x + (y + z)・・・(異なる通貨を含む)
- 分配則: x * m + y * m = (x + y) * m ・・・(異なる通貨を含む)

```scala
property("reduce(c1(n1) + c2(n2)) == c2(n1 / r + n2)") =
  forAll(intGen, intGen, rateGen) { case (n1, n2, rate@((src, dst), r)) =>
    val (c1, c2) = mapPair(src, dst)(constructor)
    val reduce   = (e: Expression) => bank(rate).reduce(e, dst)

    reduce(c1(n1) + c2(n2)) == c2(n1 / r + n2)
  }
property("reduce(x + y) equiv reduce(x) + y") =
  forAll(intGen, intGen, rateGen) { case (n1, n2, ((src, dst), r)) =>

    val reduce = (e: Expression) => new Bank().addRate(src -> dst, r).reduce(e, dst)
    val x = constructor(src)(n1)
    val y = constructor(dst)(n2)

    reduce(x + y) equiv reduce(x) + y
  }
property("(x + y) + z = x + (y + z)") =
  forAll(triple(moneyGen), rateGen) { case ((x, y, z), ((src, dst), r)) =>
    implicit val _ = compair(new Bank().addRate(src -> dst, r), dst) _

    (x + y) + z ==== x + (y + z)
  }
property("x * m + y * m = (x + y) * m") =
  forAll(double(moneyGen), intGen, rateGen) { case ((x, y), m, ((src, dst), r)) =>
    implicit val _ = compair(new Bank().addRate(src -> dst, r), dst) _

    x * m + y * m ==== (x + y) * m
  }
```

[gist: ch15 mixed concurrencies - ch16 abstraction finally](https://gist.github.com/yasuabe/c56140e53411bda6b137f84bc4ef38be)

###プロダクトコード
16章終わりの時点のプロダクトコードの状態。

```scala
trait Expression {
  def *(multiplier: Int):    Expression
  def +(addend: Expression): Expression
  def reduce(bank: Bank, to: String): Money
}
class Money(private[Chapter16] val amount: Int, val currency: String) extends Expression {
  def *(multiplier: Int): Money    = Money(amount * multiplier, currency)
  def +(addend: Expression): Expression = Sum(this, addend)

  def reduce(bank: Bank, to: String): Money =
    Money(amount / bank.rate(currency -> to), to)
  override def equals(a: Any): Boolean = a match { case another: Money =>
    another.amount   == amount &&
    another.currency == currency
  }
}
case class Sum(augend: Expression, append: Expression) extends Expression {
  def *(multiplier: Int):    Expression = Sum(augend * multiplier, append * multiplier)
  def +(addend: Expression): Expression = Sum(this, addend)

  def reduce(bank: Bank, to: String): Money = {
    val amount = augend.reduce(bank, to).amount + append.reduce(bank, to).amount
    Money(amount, to)
  }
}
object Money {
  def apply(amount: Int, currency: String) = new Money(amount, currency)
  def dollar(amount: Int): Money = Money(amount, "USD")
  def franc(amount: Int):  Money = Money(amount, "CHF")
}
type CurrencyPair = (String, String)
class Bank(private val rates: Map[CurrencyPair, Int] = Map.empty) {
  def reduce(source: Expression, to: String):   Money = source.reduce(this, to)
  def addRate(fromTo: CurrencyPair, rate: Int): Bank  = new Bank(rates + (fromTo -> rate))
  def rate(fromTo: CurrencyPair): Int = rates.getOrElse(fromTo, 1)
}
```
以下、プロダクトコードで気になる点

- `Money` が `Expression` を `extends` しているのが気になる。Scalaで FPするなら型クラスなどを利用した方がいいかもしれない。
- `Expression` 以下のクラスが `reduce` の引数として `Bank` を参照しているのが気になる。FPでいくなら、`Reader` モナドなどを用いて計算の「文脈」として換算レートが与えられるなどの方が、`Bank` のメタファーよりも自然かもしれない。
- `amount` は別に `private` じゃなくても良い気がする。ただの `case class` でよければ、プロダクトコードもプロパティベーステストも格段にシンプルになる。
- （通貨を String型で表していることや、レートや金額などの数値の型が適当なことなども気になるが、プロパティベースTDD練習用のお題だからこんなもので良しとする。）

##所感
- OOP色が濃い題材をFP寄りの手法でやったのでいろいろ無理があり、最後のプロダクトコードにもパラダイムのギャップの違い[^paradigm]がにじんでる気がするが、最初からFPでまっすぐ書けばコーディングの過程も結果のコードもスッキリしたものになったと思う。
- 2002年時点で既にOOPの大家だった著者が、TDD指南の題材としてFPとも親和性が高そうな、イミュータブルで演算が閉じている[^ddd]テスト対象を選んでいるのが、現代の視点で見てもちょっと面白い。
- 汎用性高そうでイディオマティックなプロパティが結構多い。普通の数学の代数的な法則なども頭の引き出しに用意しておくとはかどりそう。
- 思いついたプロパティが正しいか、あるいは不十分だったり冗長だったりしていないか判断が難しいが、あまりここで悩みすぎても [Analysis Paralysis] (https://en.wikipedia.org/wiki/Analysis_paralysis) に陥りそうなので、適当な見切りが必要かもしれない。
- ScalaCheck自体、あまり勉強してないので、もうちょい慣れが必要。`suchThat`で`give up`したら、`retryUntil`を使えば何とかなる場合があることなどは今回学んだ。(参考：[29 GIFs Only ScalaCheck Witches Will Understand](http://nerd.kelseyinnis.com/blog/2015/01/14/29-GIFs-only-scalacheck-witches-will-understand/)）

[^paradigm]: [wikipedia](https://en.wikipedia.org/wiki/Programming_paradigm) によれば、命令型に手続き型とオブジェクト指向が含まれ、宣言型に関数型と論理型が含まれる。なのでOOとFPは同一階層の異なるパラダイムではなく一階層上ですでに別物なので、より一層根本的な発想の違いがある。
[^ddd]: DDD の Declarative Style に通じるものがある。Side effect free functions や Closure of Operation など。

[^homomorphic]: 例えば $\log(a \times  b)= \log(a) + \log(b)$のような、線形代数などでおなじみの準同型ぽいプロパティが得られる。