Documentation

You are viewing the documentation for the 2.1.5 release in the 2.1.x series of releases. The latest stable release series is 2.4.x.

§マクロによる JSON インセプション

このドキュメントは、当初 Pascal Voitot (@mandubian) の記事 mandubian.com として公開されたものです

Scala マクロは Scala 2.10.0 においてまだ実験的な位置付けであるため、この機能もあくまで試験的なものです。Scala の実験的な機能を利用したくない場合は、完全に等価である Reads/Writes/Format を手書きしてください。

§ケースクラスのためにデフォルトの 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 シリアライザ/デシリアライザを以下のようなものにしました。

しかし、一部の人にとっては、これら利点はケースクラスそれぞれについてのコード量増加を正当化するほどではありませんでした。

一方で私達はこのアプローチ自体は正しいと信じているので、追加で次のものを提案しました。

これらの追加によって、コード量の増加を抑えつつ、機能的には先ほどの「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._
// functional.syntax._ はマクロ自身が管理しているので、インポートしないことに気を付けてください

implicit val personReads = Json.reads[Person]

// これは、次のように書くのと完全に等価です

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]

§特別なパターン

import play.api.libs.json._

case class Person(name: String, age: Int)

object Person{
  implicit val personFmt = Json.format[Person]
}
import play.api.libs.json._

case class Person(names: List[String])

object Person{
  implicit val personFmt = Json.format[Person]
}

§既知の制限

次ページ: JSON リクエストとレスポンス