この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
モバイルアプリサービス部の五十嵐です。
最近、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)
この暗黙のパラメータ rds
に creatureReads
が与えられていることがわかります。
余談
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回目に読んでようやくなんとなく理解できたくらいなので、最初は理解できなくても足りない知識を補いつつ一周してまた読んで見るということを続けると良いと思いました。