[Scala]PlayFrameworkのReads[T]を読み解く

モバイルアプリサービス部の五十嵐です。

最近、ScalaとPlayFrameworkをはじめました。

Scalaの初学者がつまずくポイントとしてPlayFrameworkのJSONコンビネーターがあると思います。私もScalaを学び始めた頃は意味がわからずおまじないのように書いていたのですが、最近ようやく読み解けるようになってきたので、今回はこれを紐解いてみます。

ScalaJsonCombinators (Version 2.2.x)

使い方

まずは公式のサンプルから使い方から見ていきましょう。

サンプルでは Creature というcase classを対象に、 JsObject型 から Creature型 に変換する例が紹介されています。

case class Creature(
  name: String, 
  isDead: Boolean, 
  weight: Float
)
import play.api.libs.json._
import play.api.libs.functional.syntax._

implicit val creatureReads = (
  (__ \ "name").read[String] and
  (__ \ "isDead").read[Boolean] and
  (__ \ "weight").read[Float]
)(Creature)
scala> val js = Json.obj( "name" -> "gremlins", "isDead" -> false, "weight" -> 1.0F)
scala> js.validate[Creature]
res1: play.api.libs.json.JsResult[Creature] = JsSuccess(Creature(gremlins,false,1.0),)

JsSuccess から Creature を取り出せば、 JsObject型 から Creature型 への変換の完成です。

applyの省略と部分適用された関数

まずはReadsの定義になる creatureReads の正体を見てみましょう。

((__ \ "name").read[String] and (__ \ "isDead").read[Boolean] and (__ \ "weight").read[Float])and は 前後を合成して FunctionalBuilder を作る関数で、最終的には FunctionalBuilder[Reads[String ~ Boolean ~ Float])] 型になります。また、その後ろにカッコがあるということは、 .apply 関数が省略されていることがわかります。

次に、 FunctionalBuilder.apply のシグネチャを見てましょう。

def apply[B](f: (A1, A2, A3) => B)(implicit fu: Functor[M]): M[B]

この先かなり複雑なので深追いすると帰ってこれなくなりそうなので、第一引数だけ見ることにします。第一引数は (A1, A2, A3) => B をシグネチャとする関数オブジェクトです。つまり、 (...)(Creature) で与えられた Creature は先のシグネチャを取る関数オブジェクトであることがわかります。

Creature は case class なのになぜ関数オブジェクトになるのか、それは Creature.apply(String, Boolean, Float) が省略された部分適応された関数だからです。部分適用された関数を作るとき、省略されたパラメータを _ で表現します。例えば、 Creature.apply の第二引数を省略したを部分適用された関数を作る場合は Creature.apply("gremlins", _: Boolean, 1.0F) のように書きます。また、全てのパラメータを省略した部分適用された関数を作りたい場合は Creature.apply _ のように、パラメータ全体を _ で省略できます。さらに _ は省略可能で、さらにさらに .apply も省略可能なので (...)(Creature) という形になります。

creatureReads の式を省略せずに書くと以下のようになります。

implicit val creatureReads: Reads[Creature] = (
  (__ \ "name").read[String] and
  (__ \ "isDead").read[Boolean] and
  (__ \ "weight").read[Float]
).apply(Creature.apply(String, Boolean, Float))

implicit val

さて、 creatureReads は明示的にはどこにも使われていませんが、 implicit val であるということは暗黙のパラメータとして使われているはずです。それは、上記の例で言うと js.validate[Creature] の暗黙のパラメータとして利用されています。validate関数のシグネチャは以下のとおりです。

def validate[A](implicit rds: Reads[A]) = rds.reads(this)

この暗黙のパラメータ rdscreatureReads が与えられていることがわかります。

余談

Reads[A]の定義の上に @implicitNotFound という見慣れぬアノテーションがありました。これはコンパイラが implicit されたメソッドやパラメータを見つけられない時に出力されるものです。もし implicit を利用するコードを自分で書く場合はヒントとして書いてあげると親切でしょう。私はいつもこのエラーでコンパイラに怒られてます。

/**
 * Json deserializer: write an implicit to define a deserializer for any type.
 */
@implicitNotFound(
  "No Json deserializer found for type ${A}. Try to implement an implicit Reads or Format for this type."
)
trait Reads[A] {

所感

Scalaは省略記法が多く慣れるまでは難しいですが、このように1つ1つ紐解いていけば読めるようになります。また JSONコンビネータの公式ドキュメントは、これまでの変遷が書かれているので最初から読んでいくと理解しやすいです。

といっても私も1回目2回目に読んだ時はちんぷんかんぷんで、3回目に読んでようやくなんとなく理解できたくらいなので、最初は理解できなくても足りない知識を補いつつ一周してまた読んで見るということを続けると良いと思いました。

参考