Play2 の Json と型クラス


クリエイティブ・コモンズ・ライセンス

Iterateeはオワコンになるかも

(すでにHaskell界隈やScalaz界隈ではオワコン)

https://twitter.com/jroper/status/456759337470291968

Haskell界隈の事情(Iterateeはオワコンらしいが、それ以外で乱立)

https://twitter.com/ma0e/status/463982747338297344

playのpull reqがrejectされたイラつきをわざわざ自分でまとめたtogetter


Play2 の pull request への態度について

社内Scala事情


  • 社内のScalaのプロジェクトはほぼPlay2
  • Scalaのプロジェクト6〜7個以上ある?(自分も把握できてない)
  • Scalaを書いてるプログラマは30人以上?( 出典@mesoさん )

以下の様なことを話したいが、詰め込み過ぎたので、全部丁寧に話せるか謎


  • 型クラスとは?
  • play2に存在する型クラス
  • Scalaにおける型クラスとは?
  • ApplicativeMonad
  • Play の ReadsWrites などが、なぜあのような書き方をするのか?
  • Play と Scalaz の比較

これから話すことは、どのversionでもそれほど大きく変わってませんが、特に言及がない場合は、play2.2.3とします

最終的な目標


case class Location(lat: Double, long: Double)
case class Resident(name: String, age: Int, role: Option[String])
case class Place(name: String, location: Location, residents: Seq[Resident])

(playの公式ドキュメントより引用)


これを理解できるように



implicit val locationReads: Reads[Location] = (
  (__ \ "lat").read[Double](min(-90.0) keepAnd max(90.0)) and
  (__ \ "long").read[Double](min(-180.0) keepAnd max(180.0))
)(Location.apply _)

implicit val residentReads: Reads[Resident] = (
  (__ \ "name").read[String](minLength[String](2)) and
  (__ \ "age").read[Int](min(0) keepAnd max(150)) and
  (__ \ "role").readNullable[String]
)(Resident.apply _)

implicit val placeReads: Reads[Place] = (
  (__ \ "name").read[String](minLength[String](2)) and
  (__ \ "location").read[Location] and
  (__ \ "residents").read[Seq[Resident]]
)(Place.apply _)

この2つの違い


implicit val placeReads: Reads[Place] = (
  (__ \ "name").read[String](minLength[String](2)) and
  (__ \ "location").read[Location] and
  (__ \ "residents").read[Seq[Resident]]
)(Place.apply _)


implicit val placeReads: Reads[Place] = for{
  name      <- (__ \ "name").read[String](minLength[String](2))
  location  <- (__ \ "location").read[Location]
  residents <- (__ \ "residents").read[Seq[Resident]]
} yield Place(name, location, residents)

for式 = モナド = カッコイイ!


と思ってる人はまだScala初心者(?)

あんな気持ち悪い(?)書き方をしているのは理由がある

勝手に3種類くらいに分類


  1. HaskellやScalazにもある型クラス
  2. Json関連の型クラス
  3. その他の型クラス

HaskellやScalazにもある型クラス


Monoid, Reducer, Functor, InvariantFunctor, ContravariantFunctor, Applicative, Alternative


  • 実際それほど汎用的に使われてるわけではない?
  • Jsonのために導入された感じ

Json関連の型クラス


Reads, Writes, OWrites, Format, OFormat


  • これらが重要なので、詳しく話したいが

その他


Writeable, ContentTypeOf, Formetter, QueryStringBindable, PathBindable, JavascriptLitteral


  • WriteableContentTypeOfは、普通にやるぶんには、そこまで意識する必要ない
  • 独自のオブジェクトを、レスポンスとして返却する場合に使う(たとえば play-json4s )

Haskellで表現(1)


-- ちょっと省略してるので不正確
data JsResult a = JsSuccess a | JsError

class Writes a where
  writes :: a -> JsValue

class Writes a => OWrites a where
  writes :: a -> JsObject -- Haskellでは本当はオーバーライドは不可能

Haskellで表現(2)


class Reads a where
  reads :: JsValue -> JsResult a

class (Writes a, Reads a) => Format a where

class (OWrites a, Reads a) => OFormat a where

Haskellで表現(3)


-- https://github.com/ekmett/contravariant/blob/v0.5.2/Data/Functor/Contravariant.hs#L76
class ContravariantFunctor a where
  contramap :: (a -> b) -> f b -> f a

Haskellで表現(4)


instance Alternative JsResult where
  -- 実装は略

instance Alternative Reads where
  -- a -> f b において、fがAlternativeなら
  -- 自動的にa -> f bもAlternative

instance ContravariantFunctor Writes where
  -- Writesは実質単なる a -> JsValue という関数と同型なので
  -- ContravariantFunctorになるのは当たり前

型クラスを一から丁寧に説明していたら、play2の型クラスの説明ができないので、他の資料に頼る


勘違いされがちなこと


  • モナドは型クラスの1つにすぎない。そういう意味では何も特別ではない
  • Scalaのimplicitは、ある意味型クラスのために導入されたという経緯(?)
  • おだすきせんせーの論文
  • implicitは型クラス以外のものにも使えてしまう
  • 型クラスとしてのimplicitは便利なのでどんどん使おう(?)

Scalaでの型クラス


  • 型クラスはtraitclassで定義
  • 型クラスのインスタンス定義方法は、以下のどれか

    • implicit val (lazy付く場合も)
    • implicit def (引数ある場合とない場合と両方あり得る)
    • implicit object
  • 使う側は、implicit parameterで受け取る

playでの具体例

型クラス定義



trait Reads[A] {
  def reads(json: JsValue): JsResult[A]

  // その他のメソッド
}


Readsのソースコード

型クラスのインスタンス定義


implicit object StringReads extends Reads[String] {
  def reads(json: JsValue) = json match {
    case JsString(s) => JsSuccess(s)
    case _ => JsError(Seq(JsPath() -> Seq(ValidationError("error.expected.jsstring"))))
  }
}

Readsのソースコード

なぜ型クラスを受け渡すのにimplicitを使うのか?

型クラスは、型によって値が一つに決まるから

(むしろ、そうでなければ型クラスと呼ばない?)

“型によって、値が一つに決まる”

ということは、


  • 型さえ指定すれば、インスタンスが手に入る(implicitly や Context Bound)
  • 変数名を意識する必要ない、するべきでない
  • 名前は重要ではない。型が重要
  • 名前を指定して使うことがないなら、衝突しない限り機械的な名前でいいのでは?
  • 名前を考える、名前を覚えるというコストがかかる行為せずに済む

“型によって、値が一つに決まる”

ということは、型パラメータをとらないimplicitなものは型クラスではない


  • Play2内部の以下のclass

    • Application
    • Request
    • Lang
  • scala.concurrent.ExecutionContext
  • scala.io.Codec
  • Databaseのライブラリでsessionを引き回すとか

一つに決まらない場合どうするのか?


  • Haskellならnewtype
  • ScalaだとHaskellに比べて新しい型を作るのが面倒(かつ、実行時コストかかる)

Scalaでの解決策


  • implicitな部分に明示的に引数を渡す
  • スコープで制御できるならスコープで制御

    • Scalaにはimplicitの解決に優先順位やスコープがあるので
    • 親クラスのimplicitのほうが優先度低い

    • 優先順位は複雑(Scalaのversionによって微妙に異なる?)
    • コンパニオンオブジェクトにimplicitなインスタンスあると、それを使いたい場合は便利だけど、使いたくない場合不便
  • Tagged Type ?(それほど実用されてない。デメリットもあり)

実際仕事でjodatimeのDateTime型のReadsのインスタンスがReadsのコンパニオンに存在していて、しかしそれを使いたくない自体が発生して 面倒な状況に!

https://github.com/playframework/playframework/blob/2.2.3/framework/src/play-json/src/main/scala/play/api/libs/json/Reads.scala#L267-L295

Monad と Applicative

  • MonadApplicative も単なる型クラスの1つ
  • play内部には Applicative はあるが Monad は現状では存在しない
  • Monad は必ず Applicative (Haskellでも次期versionで修正されるらしい)

Scalazのクラス図

なぜ Applicative が重要か?


  • Monad にも Applicative にもなるが、Applicative にする方法が2種類ある場合
  • (言い換えると) Monad にすることもできるが、それと整合性がない Applicative が存在する

    • ZipList
    • Play2 の JsResult, Reads
    • Scalaz の Validation

なぜ Applicative が重要か?


  • Monad にはならないが Applicative になるものが存在する

    • そういう場合には、for式が使えない
    • なので、最初に出した、気持ち悪い(?)記法
    • もしくは、Scalazの |@| という謎な記号

やっとplayの話


  • playの ReadsApplicative
  • Scalaz知ってる人にとっては、以下の様なものだと思えばいい

type JsResult[+A] = Validation[Seq[Error], A] // ちょっと正確じゃない
trait Reads[A] {
  def reads(json: JsValue): JsResult[A]
}
  • ReadsJsValue => JsResult[A] と同型
  • JsResultApplicative
  • A => F[B] において、F[_]Applicative ならば A => F[B] 自体も Applicative

無理矢理まとめ(?)


  • 重要なのは、playのReadsは、Applicativeの仕組みによりエラーを蓄積する機能があるということ
  • JsResult 自体にエラーを含んでいるので、Reads 自体は例外を投げるべきではない
  • できるだけ宣言的に書く
  • Readsは合成可能
  • エラーメッセージの国際化の機能まで組み込まれてる(?)

罠もある


  • インスタンスが1つだけなら、基本的にコンパニオンオブジェクトに定義すると、勝手にスコープに入るので便利
  • ReadsWrites も定義するなら、別々に定義するのではなく、Format で一緒に定義したほうが整合性がとれて良い
  • マクロで定義する機能もあるが、apply が複数あると使えなかったり、case classのフィールド名変えるだけでJsonのkey変わってしまうので微妙

ひたすら型クラスや難しい話をしたけど、型クラス以前にScalaにおける関数型プログラミングで大事なこと


  • 例外をあまりつかわない(scala.util.Tryも)
  • 副作用のあるものとないものを分離
  • リフレクションできるだけ使わない
  • できるだけimmutable
  • sealedを使って代数的データ型

Scala祭!

おわり