タイトルにわざと色々詰め込んでとても長くしてみましたが、一言で言うと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まで続く
何に使うか?というと、以下のように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な型クラスがあります。
そして、今さっき気がついたというか、頭のなかで色々繋がったのですが、play-jsonにはFunctionalCanBuildという以下のような型クラスがあります
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ページ目以降)
- https://speakerdeck.com/mpilquist/a-tour-of-functional-structures-via-scodec-and-simulacrum
- http://mpilquist.github.io/blog/2015/06/18/invariant-shadows/
- http://mpilquist.github.io/blog/2015/06/22/invariant-shadows-part-2/
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出す予定です。
*1:ekmett/semigroupoidsにあるApply https://github.com/ekmett/semigroupoids/blob/v5.0.0.4/src/Data/Functor/Bind/Class.hs#L111-L126