ContravariantやInvariantなApplicativeとscalaz.Equal, Orderとscalaprops.Cogenと各種シリアライザ―(scodec, argonaut, play-json, msgpack4z)

タイトルにわざと色々詰め込んでとても長くしてみましたが、一言で言うとContravariantなApplicative使うとそれらが共通化出来て便利なことに気がついたのでpull reqしました。という内容です。


https://github.com/scalaz/scalaz/pull/994

一ヶ月くらい前に以下のような記事を書きました


ekmett先生のdiscriminationというライブラリの動画を見たので雑に要約してみた



その中で、以下のような流れがあったのを覚えていますね?

( ^ω^) < しかしContravariantなApplicativeは・・・?

⊃ Contravariant な Applicative ⊂


( ^ω^)

≡⊃⊂≡


( ^ω^) < 存在するのじゃ!

_人人人人人人人人人人人人人_

> Contravariant な Applicative <

 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄


( ^ω^) < ContravaiantなApplicativeを、Divisibleと呼ぶことにするぞ

⊃ Divisible ⊂


そのDivisibleです。Divisibleを使うと、どう共通化出来て便利なのか?の前に、少しpurescript絡めた話をしないといけません。

1週間くらい前に、purescriptの作者が、ekemtt/contravariantに以下のようなissueをたてていました

Refine Divisble and Decidable?

内容は

「ApplicativeとApply*1が分けれるように、DivisibleやDecidableは分けれるけど、なんで分けてないの?」


みたいな感じでした。それに対してekmettせんせーは


HaskellではMonoidがSemigroupを継承してないから、それやっても色々面倒なんだよ。HaskellでMonoidがSemigroupを継承するようになったらやるよ。それまでこのissueはopenのままにしておくよ。purescriptでは分けたらいいと思うよ」


みたいな感じでした。

よってpurescriptのcontravariantのライブラリには、Divisible(ContravariantなApplicative)を分けて、Divide(ContravariantなApply)や、Decidableを分けたものも無事入ったようです。



https://github.com/purescript/purescript-contravariant/pull/5


数日前にそんなことがあったわけですが、べつにpurescriptに入っても、それほど便利じゃないならscalaz本体に入れるかどうかは微妙な気はしていました。

で、さらにここ2日くらい、猫のgitter https://gitter.im/non/cats


「InvariantなApplicative」とか「ContravariantなApplicative」みたいな話がでていました。


なので猫のgitterに「ContravariantなApplicativeはDivisibleっていうんだよ(ドヤッ」みたいな発言をしてekmettせんせーのgithubのURL貼り付ける、などをしていました。



それで、改めて考えると、1ヶ月前に書いたやつにも出てくる通り、scalaz.Equalやscalaz.Orderは、Divisibleになりますね?
そして、さらに「大抵のシリアライザ」や「scalaprops.Cogen」もDivisibleになることに気が付きました。Divisibleになっても使い道がないと嬉しくないですが、使い道あることに気が付きました。

たとえば、scalaprops.Cogenは、コード生成をした以下のようなものがあります

  final def from2[A1, A2, Z](f: Z => Option[Tuple2[A1, A2]])(implicit A1: Cogen[A1], A2: Cogen[A2]): Cogen[Z] =
    // 実装省略

  final def from3[A1, A2, A3, Z](f: Z => Option[Tuple3[A1, A2, A3]])(implicit A1: Cogen[A1], A2: Cogen[A2], A3: Cogen[A3]): Cogen[Z] =
    // 実装省略

  // 22まで続く

https://github.com/scalaprops/scalaprops/blob/v0.1.13/gen/src/main/scala/scalaprops/CogenInstances.scala


何に使うか?というと、以下のようにcase classがあった場合に、case classのそれぞれの要素がCogenのインスタンスならば、かなり少ないコードでそのcase classのCogenも生成できる、というやつです

case class User(id: Int, name: String)

object User {
  implicit val userCogen: Cogen[User] =
     Cogen.from2(unapply)
}


マクロやshapeless使えばいいわけですが、マクロはマクロでトレードオフがあったり、shapelessに依存させたくない面もあるので、コード生成してしまったほうがよい場合もあるわけです。

さて、よく考えると、このパターンは、scalaz.Equalやscalaz.Orderでも使えますね?

case class User(id: Int, name: String)

があった場合に、idのIntとnameのStringが両方同じ(等価)だったらUser自体も同じ、という定義をするはずだし、それはIntやStringのEqualのインスタンスが存在すれば、Cogenの場合と同じように、(事前にコード生成しておけば)ほぼボイラープレートなしでインスタンスが生成可能なはずです。


ちなみにscalaz本体には現状(これ書いてる時点の7.1.3 or 7.2.0-M2)そういうcase classのEqualなどを短く定義するためのヘルパーはありません。


さて、タイトルに「シリアライザ」と入れましたが、シリアライザとは、たとえば「argonaut.EncodeJson」や「scodec.Encoder」のことです。

デシリアライザ(argonaut.DecodeJsonやscodec.Decoder)はMonadになりますが、その逆のシリアライザは、大抵ContravariantなApplyもしくはApplicativeになるようです。


scodecのほうはshapeless使って頑張っていますし、argonautはコード生成をしています。

https://github.com/argonaut-io/argonaut/blob/v6.1/project/Boilerplate.scala

自分が先ほど送ったscalazへのpull reqが入れば、おそらくscodecやargonautでも使えると思います。
ただし、argonautの場合は、JsonのKeyのStringを受け取るメソッドもあるので、完全にargonaut側でボイラープレートを無くせるわけではなさそうですが。


さらに言えばplay-jsonのOWriteもContravaiantなApplyになるでしょう。そしてplay-jsonにはそもそもContravariantな型クラスがあります。

https://github.com/playframework/playframework/blob/2.4.2/framework/src/play-functional/src/main/scala/play/api/libs/functional/Functors.scala#L22-L26

そして、今さっき気がついたというか、頭のなかで色々繋がったのですが、play-jsonにはFunctionalCanBuildという以下のような型クラスがあります

https://github.com/playframework/playframework/blob/2.4.2/framework/src/play-functional/src/main/scala/play/api/libs/functional/Products.scala#L8-L14

case class ~[A, B](_1: A, _2: B)

trait FunctionalCanBuild[M[_]] {

  def apply[A, B](ma: M[A], mb: M[B]): M[A ~ B]

}


よく考えると、この FunctionalCanBuild と Contravariant をあわせると、自分が入れようとしてるDivide(ContravariantなApply)と完全に同じですね。



さて、というわけでpull reqは出したばかりなので、このblog書いてる時点では無反応ですが、入るといいなー。
まぁそもそも最近は反対意見なければ勝手にself mergeしてるので、そうなるかもしれません?


gitterの会話の雰囲気では、猫の人達もたぶん同じようなもの入れるんじゃないでしょうか。



さて、さらに発展的な内容として、「エンコーダーデコーダー(シリアライザとデシリアライザ)を合わせたもの」は、ApplicativeかつDivice(ContravaiantなApply)みたいなことになって、それは「InvariantApplicativeって呼べばいいのかな?」みたいな話が猫のgitterであったり、scodec作者がそんな発表してたりblog書いてました。(スライドは53ページ目以降)


InvariantMonadという言葉もでてきますね。
InvariantなApplicative?は、自分もmsgpack4z作ってるときにたしかに欲しい気がしました。msgpack4z.MsgpackCodecというのは、所詮シリアライザとデシリアライザを合わせたやつなので、scodecと全く同じようにInvariantなApplicative?のようなものになるはずです。

https://github.com/msgpack4z/msgpack4z-core/blob/v0.1.4/src/main/scala/msgpack4z/MsgpackCodec.scala


このInvariantなApplicativeが、すでにekmettライブラリの何処かに存在するなら、安心して移植するだけ?なのですが、自分が探した限り見つかってないのと、そもそも本当に存在するべきなのか?その場合のlawがどうなるのか?
あたりが曖昧なので、猫の人達というかscodec作者にまかせて、いい感じの結論出たらscalaz本体に入れるかもしれません。


あと、話しが前後するというか書き忘れましたが、Decidableと呼ばれるContravariantなAlternative(scalazだとApplicativePlus)は最初のpull reqでは入れてませんが、DivideとDivisibleのpull reqがmergeされたら、たぶん別pull req出す予定です。