LeanProps の紹介

TSUYUSATO Kitsune (@MakeNowJust)

前回のあらすじ

ある日のこと

https://github.com/rudymatela/leancheck

https://matela.com.br/paper/rudy-phd-thesis-2017.pdf

ひと目で、尋常じゃないライブラリと見抜いたよ

(Haskell のライブラリなので)

Scala に移植してみよう!

本編

自己紹介

  • TSUYUSATO Kitsune (@MakeNowJust)
  • 普段は TypeScript とかを書いている
  • Crystal というプログラミング言語のコントリビュータ
  • 形式言語が好き

まず初めに、

LeanCheck とは?

  • Haskell のライブラリ
  • "simple enumerative property-based testing library"
  • 実装が比較的小さい
    (Test.LeanCheck.Coreはコメントを除いて 185 行くらい)
> holds 100 $ \xs-> sort (sort xs) == sort (xs :: [Int])
True

LeanCheck とは?

  • SmallCheck のような感じで、小さいものから順番にチェックしていく方針
  • ランダムにデータを生成するのはサポートしていない
  • Generic を使ったデータを生成するクラスの自動導出
  • Template Haskell を使ったデータを生成する型クラスの自動導出
  • 関数の生成をサポートしている
  • 関数に対するShowクラスのインスタンスを提供している

(個人的には下の二つが面白いと思った)

早速、Scala に移植してみた

https://makenowjust.github.io/leanprops/

LeanProps という名前になった

(leancheck4s という名前が
当時はなぜか思い付かなかった‥‥)

LeanProps で出来ること

  • Intなどの整数、Doubleなどの浮動小数点数、
    いくつかのコレクション、関数などの生成
  • ↑ の型の値を文字列にしてて表示する機能(pretty print)
  • property-based testing の基本的なサポート
  • Magnolia による生成と表示を担当する型クラスの自動導出
scala> import codes.quine.leanprops._
import codes.quine.leanprops._

scala> inspect { (x: Boolean, y: Boolean) => x && y }
res0: String =
((x, y) => (x, y) match {
  case (true, true) => true
  case _            => false
})

scala> holds(100) { (xs: List[Int]) =>
     |   xs.sorted.sorted == xs.sorted
     | }
res1: Boolean = true

動いています

LeanProps の主要な型

  • trait Listable[A]:
    Aが自動生成可能なことを表す型クラス
  • trait Inspectable[A]:
    Aが文字列にして表示可能なことを表す型クラス
  • class Tiers[A]:
    Stream[Seq[A]]をラップしたデータ構造
  • class WithInspectConfig[A]:
    InspectConfigを持った Reader Monad

一番重要なのはtrait Listable[A]。

trait Listable[A]

trait Listable[A] {
  def tiers: Tiers[A]
}
  • Tiers[A]はStream[Seq[A]]のラッパ
  • なぜ単純なStream[A]ではなくStream[Seq[A]]なのは
    データを組み合わせられるようにするため

どうしてStream[A]じゃダメ?

仮にListable[A]がこうだったとする。

trait Listable[A] {
  def list: Stream[A]
}

list[A] = Listable[A].listとする。

どうしてStream[A]じゃダメ?

  • list[Int] = Stream(0, 1, -1, ...)

  • そこでlist[(A, B)]を考えると、単純な実装だと‥

    list[(A, B)] = for {
      x <- list[A]
      y <- list[B]
    } yield (x, y)
    

どうしてStream[A]じゃダメ?

  • つまり、

    Stream((list[A](0), list[B](0)),
           (list[A](0), list[B](1)),
           ... (list[B]が終了するまで続く),
           (list[A](1), list[B](0)),
           ...)
    
  • (Int, Int)の場合、
    左側に0しか出てこなくなる。

どうしてStream[A]じゃダメ?

  • そこでtiers[A]: Stream[Seq[A]]を考える。

  • tiers[Int] = Stream(Seq(0), Seq(1), Seq(-1), ...)

  • tiers[(A, B)]は次のような感じ、

    def prod[A, B](xs: Seq[A], ys: Seq[B]) = for (x <- xs; y <- ys) yield (x, y)
    Stream(prod(tiers[A](0), tiers[B](0)),      // 添字の合計が0
           prod(tiers[A](0), tiers[B](1))
             ++ prod(tiers[A](1), tiers[B](0)), // 添字の合計が1
           prod(tiers[A](0), tiers[B](2))
             ++ prod(tiers[A](1), tiers[B](1))
             ++ prod(tiers[A](2), tiers[B](0)), // 添字の合計が2
           ...)
    
  • こうすると、無限リストでもいい感じに積を取れる。

trait Listable[A]

  • SetやMapの場合は重複しないように生成したり、
    いくつか工夫する必要がある。
  • Function1の場合はMapとほとんど同じなのだけど、
    生成される関数を全域にするためフォールバック用の値も
    同時に生成する。
  • 詳しくはソースコードを読んでください。

関数の表示

(さすがに時間が無さそうなので三行で)

  1. 引数の組を生成して、関数を呼び出して、返り値を記録
  2. 返り値の同じ引数の組を_で上手い具合に置き換えていく
    (実際はそんなに上手くやっていない)
  3. それらを並べて表示する。

引数を自動生成できるんだから、
たくさん呼び出して返り値を記録すれば
それっぽいmatch式が作れるよね、というノリ。

trait Inspectable[A]

trait Inspectable[A] {
  def inspect(x: A): WithInspectConfig[A]
  def bindtiers(x: Try[A]): Tiers[(Seq[Seq[String]], Try[String])]
}

このbindtiersが、引数と返り値を記録するメソッド。

現実は厳しい‥‥

scala> inspect { (xs: List[Int]) => xs.head }
res3: String =
(x => x match {
  case List()        => throw new NoSuchElementException("head of empty list")
  case List(0)       => 0
  case List(0, 0)    => 0
  case List(1)       => 1
  case List(0, 0, 0) => 0
  case List(0, 1)    => 0
  case List(1, 0)    => 1
  case List(-1)      => -1
  ...
})

ぶっちゃけBooleanのときくらいしか上手くいかない。

思ったことなど

  • Streamは難しすぎる。
    すぐに OOM と StackOverflow を起こす
  • Magnolia の黒魔術感‥‥。
  • テストは μTest と sbt-doctest で書いた。
    doctest 良い
  • ドキュメントは mdoc と Docusaurus で作った。
    大したライブラリじゃないのに無駄に立派になってしまう

今後の目標、やっていき

  • いくつかの名前のリファクタリング。
    WithInspectConfigって名前どうなの?
  • Listable、Inspectableのインスタンスを増やす
  • cats・scalazの型のインスタンスを実装する
  • shapelessでの自動導出
  • sbtとの統合
  • disciplineのようなインターフェース(?)

ありがとうございました 😄