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も直すのとか地味に面倒・・・
もしくは「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必要なのでしょう