§マクロによる JSON インセプション
このドキュメントは、当初 Pascal Voitot (@mandubian) の記事 mandubian.com として公開されたものです
§ケースクラスのためにデフォルトの Reads/Writes/Format を書くのは面倒くさい!
ケースクラスのための Reads[T]
を書く方法を思い出してみましょう。
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class Person(name: String, age: Int, lovesChocolate: Boolean)
implicit val personReads = (
(__ \ 'name).read[String] and
(__ \ 'age).read[Int] and
(__ \ 'lovesChocolate).read[Boolean]
)(Person)
ひとつのケースクラスのために、4 行のコードを書きました。
あなたはどう思いますか?
人によっては Reads[TheirClass]
を書くというのは格好良くない、と考えるようです。その理由は、Jackson や Gson のような Java の JSON フレームワークが通常そのようなコーディングを全く必要とせずに、カーテンの裏側でよしなに計らってくれるから、というものです。実際、そのように考えている人々からの改善要望も聞いていました。
そして、私たちは議論の結果、 Play 2.1 の JSON シリアライザ/デシリアライザを以下のようなものにしました。
- 完全に型安全
- 全てがコンパイルされる
- 実行時にはイントロスペクション/リフレクションを利用した処理が一切行われない
しかし、一部の人にとっては、これら利点はケースクラスそれぞれについてのコード量増加を正当化するほどではありませんでした。
一方で私達はこのアプローチ自体は正しいと信じているので、追加で次のものを提案しました。
- JSON 簡易文法
- JSON コンビネータ
- JSON 変換子
これらの追加によって、コード量の増加を抑えつつ、機能的には先ほどの「4 行の追加コード」と同じものを実現します。
§ミニマリストになろう
私達は完璧主義者として、先ほどのコードの新しい記述方法を提案します。
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class Person(name: String, age: Int, lovesChocolate: Boolean)
implicit val personReads = Json.reads[Person]
たったの 1 行です。
今、こんな疑問が浮かんだのではないかと思います。
実行時のバイトコード書き換えをしている? -> いいえ
どういうこと?
勝手に作った 端から端への JSON 設計 というバズワードに倣って、これを JSON インセプション と呼ぶことにしましょう。
§JSON インセプション
§等価なコードについて
先ほどご説明したとおり、以下のコードと等価です。
import play.api.libs.json._
// please note we don't import functional.syntax._ as it is managed by the macro itself
implicit val personReads = Json.reads[Person]
// IS STRICTLY EQUIVALENT TO writing
implicit val personReads = (
(__ \ 'name).read[String] and
(__ \ 'age).read[Int] and
(__ \ 'lovesChocolate).read[Boolean]
)(Person)
§インセプションの方程式
これが颯爽と現れた インセプション の概念を説明するための方程式です。
(Case Class INSPECTION) + (Code INJECTION) + (COMPILE Time) = INCEPTION
§ケースクラス・インスペクション
想像がつくかもしれませんが、先ほどのコード等価性を確保するためには、次のものが必要です。
§インジェクションとは?
先に断っておきますが…
コードインジェクションとは、 DI のことではありません…
インセプションの裏に Spring はいません。IOC や DI はいるか、って?…いやいやいや ;)
私は、一般に「インジェクション」という言葉からは IOC や Spring がすぐ連想される、ということを理解しています。しかし、この用語の本来の意味を改めて確立しなおしたいと考えて、あえてこの用語を使います。ここでのコードインジェクションの意味は、 「コンパイル時に、コンパイル結果としての Scala の AST (Abstract Syntax Tree/抽象構文木) の中に、コードをインジェクトする」 です。
このインジェクションの結果、繰り返しになりますが、 Json.reads[Person]
はコンパイルされ、コンパイル結果の AST の中で以下のコードに置換されます。
(
(__ \ 'name).read[String] and
(__ \ 'age).read[Int] and
(__ \ 'lovesChocolate).read[Boolean]
)(Person)
これ以上でも、これ以下でもありません…
§コンパイル時
はい、すべてはコンパイル時に行われます。
実行時のバイトコードエンハンスメントはありません。
実行時のイントロスペクションもありません。
全てがコンパイル時に解決されるため、全フィールドの全ての型に要求される implicit 値が import されていない場合、コンパイルエラーが発生します。
§JSON インセプションは Scala 2.10 のマクロです
私達には以下の 3 つを実現できる Scala の機能が必要でした。
結果的には、JSON インセプションは Scala 2.10 で新たに導入された試験的な機能 Scala マクロ によって実装されました。
Scala マクロは大きなポテンシャルを持った (まだ試験的な) 新機能です。これにより以下のことができます。
また、補足として以下のことを知っておいてください。
お気づきかもしれませんが、マクロを書くというのは並大抵の仕事ではありません。マクロのコードがコンパイラーのランタイムに (または、 universe の中で) 実行されるからです。
したがって、この仕事をやり遂げるには少し頭の体操が必要です。また、 API はかなり複雑で、ドキュメントも完全ではありません。そのため、マクロを使いはじめるにあたっては相応の努力をすることになります。
Scala マクロについてはまだまだ説明したいことが沢山あるので、きっと別の記事も書くと思います。
この記事には、 Scalaマクロの正しい使い方について熟考するきっかけになって欲しい、という想いも込められています。
強力な力にはより大きな責任が伴いますから、これから一緒に話し合いを重ねて、良い作法を少しずつ確立していければ何よりです。
§Writes[T] と Format[T]
JSON インセプションは、
unapply/apply
関数の入力/出力の型が互いに対応している場合にのみ機能する、ということに気をつけてください。
もちろん、Writes[T]
や Format[T]
を インセプト することもできます。
§Writes[T]
import play.api.libs.json._
implicit val personWrites = Json.writes[Person]
§Format[T]
import play.api.libs.json._
implicit val personWrites = Json.format[Person]
§特別なパターン
- コンパニオンオブジェクト中に Reads/Writes を定義することができます
こうすることで、コンパニオンオブジェクトに対応するクラスのインスタンスを扱う際に implicit な Reads/Writes が暗黙的に推論されるので、便利です。
import play.api.libs.json._
case class Person(name: String, age: Int)
object Person{
implicit val personFmt = Json.format[Person]
}
- 現在は、フィールドがひとつしかないケースクラスの Reads/Writes を定義することもできます (2.1-RC2 までは既知の制限でした)
import play.api.libs.json._
case class Person(names: List[String])
object Person{
implicit val personFmt = Json.format[Person]
}
§既知の制限
- コンパニオンオブジェクト内で apply 関数をオーバーライドしないでください。 マクロが複数の apply 関数を持つことになり、選ぶことができません。
- Json マクロは、apply と unapply の入力/出力の型が互いに対応している場合にのみ動作します: これはケースクラスとして自然な状態です。しかし、これをトレイトで行う場合、ケースクラスに含まれることになる apply/unapply と同じものを実装しなければなりません。
- **Json マクロが Option/Seq/List/Set & Map[String, _] を受け取れる** ことは分かっています。これら以外の総称型については、テストして、もし動作しない場合は、これまで通り手動で Reads/Writes を書いてください。
Next: XML を使う
このドキュメントの翻訳は Play チームによってメンテナンスされているものではありません。 間違いを見つけた場合、このページのソースコードを ここ で確認することができます。 ドキュメントガイドライン を読んで、お気軽にプルリクエストを送ってください。