Scalacheckに標準で、Function1からFunction5まであるのですが
implicit def arbFunction1[T1,R](implicit a: Arbitrary[R] ): Arbitrary[T1 => R] = Arbitrary( for(r <- arbitrary[R]) yield (t1: T1) => r )
というように、引数無視して、戻り値は固定のなにかを返すだけです。
例えば、ScalazのmonadPlusのlawのテスト*1だったら
def laws[F[_]](implicit F: MonadPlus[F], afx: Arbitrary[F[Int]], afy: Arbitrary[F[Int => Int]], ef: Equal[F[Int]] ) = new Properties("monad plus") {
というような感じでFunctionのArbitraryを要求していますが
「戻り値固定で返すだけの函数」
では、ちょっと頼りないなーと気がついたという話です。
しかし、戻り値と返り値型に色々と制限を加えないと、なにか実用的にテストに効果がある函数を定義するのは難しいですし、
「汎用的に色々な型の函数に対してFunctionのArbitraryを定義する方法」
はそもそも存在しなそうなので、たしかにこうするより他はないですね。
ただ、Arbitraryのコンパニオンオブジェクトにおいてあるので、
"デフォルトのものを使わずに自分で作成したカスタマイズしたArbitrary[Function]を使いたい場合"
に、(implicitの優先順位やスコープなどを考えて)本当に自分が作ったものが使われているのかどうか?が、結構わかりづらくていやですね・・・
そして、ScalazのテストにおけるScalacheckのArbitrary[FunctionN]については、Intでテストしているものがほとんどです。できれば、(戻り値型と引数型同じなら)identity functionにするとか、単に+1する函数とか、もうちょっと工夫したほうが、バグみつかりやすくなるのでは?と思いました。
例えばこの件
ekmett/semigroupoids のバグ?
で
「Listのduplicated(scalazだとcojoin)は、空のリストも含むべきなのか?」
を確かめるために、デフォルトの定義でやったら、以下の2つの定義のどちらもテストが通ってしまいました
def cojoin[A](a: Stream[A]) = a.tails.toStream // 空も含むもの
def cojoin[A](a: Stream[A]) = a.tails.toStream.init //空(一番最後)をinitを使って除くもの
なので、以下のように試しにわざわざInt型をラップして、FunctionのArbitraryも独自定義(単にlistのsize取得するだけ)してテストしてみると
https://github.com/xuwei-k/scalaz/commit/6002be67abf660e7fc7793ed88327db1be246843
https://travis-ci.org/xuwei-k/scalaz/jobs/7907718#L308
見事に
a.tails.toStream
の定義のほうは、テスト失敗しました
というわけで、自分自身Extend(Cojoin)のlawの理解がまだ曖昧だけれども、Scalacheckでテスト書けば勝手に検出されるの素晴らしいですね。
HaskellでもQuickcheckでmonadなどのlawをテストする習慣もっと広まればいいのに。
あとこれにより、「semigroupoidsの件はやはりSequenceのインスタンス定義のほうがバグだろう」という思いが強まりました。しかし、haskell-jpの投稿に対しては、まだ反応ないですね・・・。やっぱりバグだったらしく、issue報告したら修正されました https://github.com/ekmett/semigroupoids/issues/8
*1:ほかにもScalazのテスト内でFunctionのarbitraryを要求するもの多数ある