Playframework2のJsonやJsResultと型クラスとそれらに関する罠


これは Play framework 2.x Scala Advent Calendar 2013の17日目です。
アドベントカレンダーが埋まらないので、また役に立つかどうかわからない微妙なネタで、7日目に続き、2回目の参加です。


そもそも、以前Play内部の型クラスについてちょっと書いたことがあります。

Play2.1のfunctional packageのApplicativeを使う

play2内部には、Applicative型クラスの他にも、Alternative(ScalazではApplicativePlusとも呼ばれる)やFunctor、Monoidなどもあります。

で、Play2内部にJsResultというデータ型があります。ものすごく簡単に説明してしまうと、Scalazでいう

Validation[Seq[エラー], 結果値]

と同じようなものです。Scalazわからない人は、とりあえずEitherと似たもの、という認識でもいいです。
https://github.com/playframework/playframework/blob/2.2.2-RC1/framework/src/play-json/src/main/scala/play/api/libs/json/JsResult.scala
ただし、簡単に合成できて、その際にエラーが勝手に蓄積されて便利です。

JsResultという名前のとおり、最初からJsonの関連のものに限定されてます。エラーの型は実際は

Seq[(JsPath, Seq[ValidationError])]

というものです。そして、このJsResultはApplicativeやAlternativeインスタンスになっています。

あと、PlayのJsObjectやJsArrayはMonoidになっています。

さて、このあたりからやっと本題ですが、それらPlay2内部の型クラスとScalazの型クラスの相互変換するものを作りました。

https://github.com/xuwei-k/play2scalaz

そして、Monoid則やAlternative則をScalazの仕組み使ってテストしたら、最初は失敗しました。なんだか詳細に説明するの面倒なので(ぉぃ)興味のある人は、コードとtravisの結果見てください。


簡単に説明すると以下の様にEqualを定義しないとだめでした

https://github.com/xuwei-k/play2scalaz/blob/d9ea768f2236a/src/main/scala/Play2Scalaz.scala#L135-L138

  • JsSuccessの場合にもPathがあるが、それは比較せず結果値のみを比べる
  • JsErrorの場合は、内部のSeqをtoSetしてから比較しないとだめ

また、JsArrayやJsObjectのMonoid則も似たような感じで、コーナーケースが存在して、厳密にはMonoid則を満たしません。
JsObjectでは、zero*1が空のjson(つまり{})、appendがdeepMergeで定義されています。

https://github.com/playframework/playframework/blob/2.2.2-RC1/framework/src/play-json/src/main/scala/play/api/libs/json/Reads.scala#L106-L107

試すと以下のようになります。

まぁキーが重複してる時点で、ほとんどありえないケースなので、それほど気にしなくてもいいかもしれませんが。
*2


たしか、keyが重複していた場合の挙動(parseする場合に、後を優先するのか、先を優先するのか?)はJsonの仕様として決まってなかったはずです。(間違ってたら教えてください)

だがしかし、PlayのJson場合

Json.parse(""" { "a": 1, "a": 2 }""")

とかしても、

case class JsObject(fields: Seq[(String, JsValue)]) extends JsValue

https://github.com/playframework/playframework/blob/2.2.2-RC1/framework/src/play-json/src/main/scala/play/api/libs/json/JsValue.scala#L164

だし、一旦重複したまま保持されるようです(2.2.2-RC1時点)。またfieldSetというのは

def fieldSet: Set[(String, JsValue)]

https://github.com/playframework/playframework/blob/2.2.2-RC1/framework/src/play-json/src/main/scala/play/api/libs/json/JsValue.scala#L198

なので、これ呼んだ時点でもKeyは重複したままだし、またequalsにfieldSet使ってるのでもう色々残念です。



というわけで、別にPlay2のJsonそのものはそこまですごくダメなわけでもないと思いますが*3
厳密に考えるとApplicative則やMonoid則を満たさない場合があったり、fieldSet呼んだ時点でもkeyが重複してる可能性があるようなので、注意しましょう。


このあたりの細かい挙動をべつに完璧に覚えておく必要はないと思いますが「Applicative則やMonoid則を満たさない場合があるんだな」というのは頭に入れておいたほうがいいかもしれません。なにかバグっぽい挙動に当たった場合

「Applicative則やMonoid則を満たす前提」

で、そこを疑わずに他の原因探っていてなかなか解決しない、というようなことにならないように。


このあたりの細かい挙動、Scalaのその他のJsonライブラリでもそれぞれ違っていたり工夫されていたりしたはずです。気が向いたら、詳細に調べて、別エントリで比較記事書きたいですね。


最後に話逸れますが、さっき言及したdeepMergeのメソッド内で、Listの末尾に :+ で何度も追加していてぉぃ大丈夫か・・・と不安になりました。*4


https://github.com/playframework/playframework/blob/2.2.2-RC1/framework/src/play-json/src/main/scala/play/api/libs/json/JsValue.scala#L242

「もう安定してきたかなぁー」と思いきや、まだまだ酷いコード残ってますね、Playさん・・・。



ちなみに、今回書いたことは、まだpull reqもissue報告もしてません。issue報告してもスルーされる気がするので迷ってます。どうしようかな・・・



追記
https://github.com/playframework/playframework/pull/2202
「Listの末尾に追加」の件は、とりあえずVectorに変えてpull reqしました。*5

*1:playだとidentityというメソッド名

*2:ただし、これ以外のパターンでも満たさない場合があるのかどうか、完全には調べきれてない

*3:マクロ使って色々頑張ってるのは、一応評価されるべき?

*4:先頭に追加していって最後にreverseか、VectorやBuffer使うべき

*5:本当は、「Listの先頭に足していって最後にreverse」などのほうが効率いいかもしれないが、テスト多くないし、同じ挙動を保証するのがそれほど簡単ではなさそうだったので