Refined なケースクラスの ScalaCheck テスト

Refined を使って制約を型で表現したケースクラスを、ScalaCheck でプロパティベーステストしてみる。

## お題
『[The Type Astronaut's Guide to Shapeless](https://underscore.io/books/shapeless-guide/)』に下のようなアイスクリームクラスがある。

```scala
case class IceCream(name: String, numCherries: Int, inCone: Boolean)
```
名前、チェリーの個数、コーンに入ってるかどうかで構成されている。これをたたき台に、まず Refined を導入して、次にプロパティベーステストを書いてみる。

次のように Refined を導入する

- 下記の制約を型として与える
 - 名前は、英数＋空白からなる２〜10文字の文字列とする
 - チェリーの個数は、1〜10個とする
- 上の制約を満たすアイスクリームを文字列から作る関数を書く。

プロパティベーステストは次のようにする

- サンプルアイスクリームのジェネレータを作る
- そのジェネレータを使って下のプロパティをテストする
 - どんなアイスクリームでも、いったん文字列にしてから元のアイスクリームに復元できる
 - もしある文字列からアイスクリームを作れたならば、そのアイスクリームを文字列化すると元の文字列に一致する。

## 実装
[ソース→Gist](https://gist.github.com/yasuabe/eefb873b02fe1e69588ac1dd47c9e59b)
#### 使用バージョン等
- [refined 0.9.3](https://github.com/fthomas/refined)
- [scalacheck 1.13.5](https://www.scalacheck.org/)
- [cats 1.5.0](https://typelevel.org/cats/)
- [shapeless 2.3.3](https://github.com/milessabin/shapeless)

### Refined による制約の表現
上の制約を refined を用いて表現すると下のようになる。

```scala
// 名前は、英数＋空白からなる２〜10文字の文字列とする
type NameP = MatchesRegex[W.`"[a-zA-Z0-9 ]{2,10}"`.T]
type Name  = String Refined NameP

// チェリーの個数は、1〜10個とする
type NumP = Closed[_1, _10]
type Num  = Int Refined NumP

case class IceCream(name: Name, numCherries: Num, inCone: Boolean)
```

名前の制約では正規表現へのマッチを表す述語 `MatchesRegex` を使った。指定する正規表現パターンは ```W.`"~~~"`.T```で囲んで、型引数として与える。

チェリーの個数の制約は述語 `Closed` で実装した。`_1`や`_10`は、`import shapeless.nat._` で使えるようになるが、``W.`10`.T`` などとしても同じ。

### Refined なインスタンスの生成
文字列からアイスクリームを作る関数 `read` は以下のように書いた。

```scala
def read(s: String): Either[String, IceCream] = {
  val params = raw"([a-zA-Z0-9 ]+),([0-9]+),(true|false)".r
  s match {
    case params(s1, s2, s3) => for {
      name <- refineV[NameP](s1)
      num  <- refine(s2.toInt)(refineV[NumP].apply)
      cone <- refine(s3.toBoolean)(_.asRight)
    } yield IceCream(name, num, cone)
    case _ => s"ERROR: $s".asLeft
  }
}
private def refine[A, B](f: => A)(g: A => Either[String, B]) =
  Try(f).fold(_.getMessage.asLeft, g)
```
正規表現で文字列片を切り出して、制約を満たせば各フィールドに変換する。制約に合わない場合 `refineV` の結果が `Left` になるので、文字列→アイスクリームへの変換全体についても `Either[String, IceCream]` となる。

アイスクリームから文字列にする `show`も下記のように定義しておく。

```scala
  def show: String = s"$name,$numCherries,$inCone"
```

### サンプルデータのジェネレータ
サンプルアイスクリームのジェネレータは以下のようになる。

```scala
object IceCreamSpec extends Properties("IceCream") {
  ...
  val genName = (for {
    len   <- chooseNum(2, 10)
    chars <- listOfN(len, oneOf(alphaChar, numChar, const(' ')))
  } yield refineV[NameP](chars.mkString)).map(_.right.get)

  val genIceCream = (genName, arbitrary[Num], arbitrary[Boolean]) mapN IceCream.apply
  ...
```
名前の生成では、ScalaCheck の `Gen` をいろいろ使って、正規表現にマッチする文字列を生成しているが、整数に範囲をつけただけのチェリーの個数は、必要な implicit インスタンスが refined-scalacheck で暗黙に提供されているので、単に `Arbitrary[Num]` を召喚するだけで利用できる。

下記のようなサンプルアイスクリームが得られる。

```
IceCream(38c8e1 d  ,1,false)
IceCream(d e2 ,1,false)
IceCream(uhbqnz 918,10,true)
IceCream(2  s X,9,false)
IceCream( 32,1,true)
IceCream(2 ,5,false)
IceCream( 3i1fdq7,9,false)
IceCream(2 a  wt 9 ,1,true)
IceCream(1cXd     t,1,false)
```

文字列のジェネレータは、アイスクリームに変換できる文字列と、ただのランダムな文字列のいずれかが生成されるように実装する。

```scala
val genString = oneOf(
  (genName, arbitrary[Num], arbitrary[Boolean]) mapN ((s, n, b) => s"$s,$n,$b"),
  asciiStr
)
```
以下のようなサンプル文字列が生成される。

```
p4dcg8 v v,6,true
4   o   m ,9,true
dw6  3i r,10,true
3t
 q,3,false
:

    yq,8,false
7
]] qrq^(
 0,1,false
```

### プロパティベースのテスト
これらを用いて、下記のようにプロパティをテストできる。

```scala
property("∀ ic: IceCream, read(show(ic))==ic") = forAll(genIceCream) { ic =>
  read(ic.show).right.get == ic
}
property("∀ s: String s.t. read(s)=Right(ic), show(ic)==s") = forAll(genString) { s =>
  read(s).map(_.show).forall(_ == s)
}
```
テスト実行結果は以下のようになる。

```
+ IceCream.∀ ic: IceCream, read(show(ic))==ic: OK, passed 100 tests.
+ IceCream.∀ s: String s.t. read(s)=Right(ic), show(ic)==s: OK, passed 100 tests.

Process finished with exit code 0
```

### Cats メモ
ジェネレータを作っているところで、Cats の apply 構文の `mapN` を使っている。そのために、スコープ内に implicit な `Apply[Gen]` が必要になるので、以下のように用意しておく。

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