Scalacheckの絶妙なバグのせいで、Scalazのバグが見つけられなかった話

scalacheckでバグ見つかる
https://github.com/rickynils/scalacheck/issues/75
内容を一言でいうと
「OpitonのArbitraryの質が悪い」
もう少し詳しく説明すると
以下のテスト

forAll { (x: Option[Int], y: Option[Int]) => 
  x.isDefined == y.isDefined
}

が、必ず成功してしまう。というものです。
ここで絶妙なのが「すべてSome、またはすべてNoneしか発生しない」のではなく「SomeもNoneも発生するけど、発生する場合に必ず両方Someか両方Noneになる」*1というバグり方をしているということです。


そして、Scalazはかなり多くの部分でScalacheckを使っているので
「このScalacheckのバグ、Scalazに影響あるかな?」
と思って試してみたら、案の定影響がありました

https://github.com/scalaz/scalaz/issues/579

見つかったのは「OptionTのApplyのインスタンス定義と、LazyOptionTのApplyのインスタンス定義が、Monadのbindの定義と整合性がなくておかしい」というバグです。

Scalacheckのバグが絶妙なのは「両方Some、もしくは両方Noneの場合は、このScalazのバグも見つからなかった」というところです。*2
*3


さて、このScalazのバグですが、ここでekmett先生のsemigroupoids内のMaybeTのApplyのインスタンスを見ると、以下のようになってます

https://github.com/ekmett/semigroupoids/blob/v4.0/src/Data/Functor/Bind.hs#L207-L209

-- MaybeT is _not_ the same as Compose f Maybe
instance (Bind m, Monad m) => Apply (MaybeT m) where
  (<.>) = apDefault

一方、現状のバグっているScalazは以下
https://github.com/scalaz/scalaz/blob/v7.1.0-M3/core/src/main/scala/scalaz/OptionT.scala#L40

def ap[B](f: => OptionT[F, A => B])(implicit F: Apply[F]): OptionT[F, B] =

https://github.com/scalaz/scalaz/blob/v7.1.0-M3/core/src/main/scala/scalaz/OptionT.scala#L87

implicit def optionTApply[F[_]](implicit F0: Apply[F]): Apply[({type λ[α] = OptionT[F, α]})#λ] =
  new OptionTApply[F] {


つまり、比較すると以下のようになっていて

  • semigroupoidsの場合は、MaybeTのApplyのインスタンスを定義するのに、MonadとBindを要求する
  • 現状のScalazは、OptionTのApplyのインスタンスを定義するのに、Applyだけを要求する(そして、その結果定義されたものが間違っている?)

semigroupoidsのほうが制限が厳しいです。
これは、semigroupoidsが正しい、つまり「MonadやBindがないと、MaybeTのApplyのインスタンスは定義できない」のだとしたら、Scalazのほうを結構直さないといけないですね。その場合色々消すだけなので、シンプルにはなりますが。ただ、バイナリ互換保ったまま7.0.xも直すのとか地味に面倒・・・

*4

もしくは「semigroupoidsのMaybeTのApplyのインスタンス定義は間違っていないけど、もっと制限を緩くできるよ」というツッコミなどあれば教えて下さい。


追記
予想通り「Monadがないと、OptionTのApplyのインスタンスは定義できない」みたいで、その方向で修正されました。
https://github.com/scalaz/scalaz/issues/581
https://github.com/scalaz/scalaz/issues/582

*1:もっと具体的に言うと、最初の一回は両方None、その後はずっと両方Someになるみたい?

*2:言い換えると、両方Someや、両方Noneの場合に限って、現状のおそらくバグっているOptionTのApplyの定義がテストをパスしてしまう

*3:これ、一番最初にOptionTがScalazに入ったScalaz6のころからずっとこの定義みたいですね・・・3年間誰も気づかなかったの・・・ https://github.com/scalaz/scalaz/blob/6.0-for-2.9.x/core/src/main/scala/scalaz/OptionT.scala#L33

*4:ちなみに、transformersでのMaybeTのApplicativeのインスタンスMonadを要求するみたいだし http://hackage.haskell.org/package/transformers-0.3.0.0/docs/Control-Monad-Trans-Maybe.html#v:MaybeT たぶんMonad必要なのでしょう