( Scala Advent Calendar 2014 の 11 日目です )
動機
- trait で表現されたデータ構造から JSON や XML へ変換するコードを自動生成したい
- つまり trait からフィールドの名前とその型情報だけ抜き出したい
- しかしマクロやリフレクションに依存したライブラリは書きたくない
- 将来いつ変わるか分からない!
- 歴史的には Manifest から TypeTag への変更といった実例あり
- 変わったときのライブラリの書き直しはしんどい
- だとしたら型情報だけ取り出してくれる中間層を用意すればいいのでは
- その中間層が Scala の内部実装を隠蔽してくれればいい!
- 将来いつ変わるか分からない!
実例
たとえば下記のようなデータ構造を表現した trait に対して
InspectorSample.scala
package example
trait SampleStructure {
def x: Int
def y: SampleGeneric[String, Long, Int]
def z: List[Int]
}
trait SampleGeneric [A, B, C]{
def a: A
def b: B
def c: Nested[Nested[Nested[C]]]
}
trait Nested[A]{
def foo: A
}
以下のように取り出せるようになります。
val digest = TypeReflector.inspect[SampleStructure]
println(digest.members.find(_.decodedName == "z").map(_.resultType.typedName))
// Some(scala.collection.immutable.List[scala.Int])
val name = for {
y <- digest.members.find(_.decodedName == "y")
b <- y.resultType.members.find(_.decodedName == "b")
} yield {
b.resultType.typedName
}
println(name)
// Some(scala.Long)
コンパイル時に消されているはずの型パラメータも問題なく取得できています(!)
次は階層を再帰的に走査する関数 dump
の例です。
InspectoSample.scala
object InspectorSample extends App {
val digest = TypeReflector.inspect[SampleStructure]
println(dump(digest))
def dump(digest: TypeDigest, indent: Int = 1): String = {
val lines = digest.typedName +: digest.members.
map { f => s"${f.decodedName} : ${dump(f.resultType, indent + 1)}" }.
map { " " * indent + _ }
lines.mkString("\n")
}
}
実行してみます。
> taupe-app/run
[info] Running example.InspectorSample
example.SampleStructure
z : scala.collection.immutable.List[scala.Int]
y : example.SampleGeneric[java.lang.String,scala.Long,scala.Int]
c : example.Nested[example.Nested[example.Nested[scala.Int]]]
foo : example.Nested[example.Nested[scala.Int]]
foo : example.Nested[scala.Int]
foo : scala.Int
b : scala.Long
a : java.lang.String
x : scala.Int
この出力により、たとえば example.SampleStructure
型のオブジェクト obj
に対して
obj.y.c.foo.foo.foo
は Int
型であるということが分かります。
念のため試してみましょう。
> ~taupe-app/console
[info] Starting scala interpreter...
scala> def obj: example.SampleStructure = ???
obj: example.SampleStructure
scala> def f = obj.y.c.foo
f: example.Nested[example.Nested[Int]]
scala> def f = obj.y.c.foo.foo
f: example.Nested[Int]
scala> def f = obj.y.c.foo.foo.foo
f: Int
カンペキです。
利用方法
- ライブラリのリポジトリ : Salad
- サンプルのプロジェクト : sample-taupe
sbt からは こんな形のビルド定義 で取り込むことができます。
dependsOn(ProjectRef(uri("git://github.com/x7c1/Salad.git#0.1"), "salad-lib"))
おまけ
用意されているマクロ TypeExpander
からも
val digest = TypeExpander.inspect[SampleStructure]
println(dump(digest))
同一の結果が得られます。
example.SampleStructure
z : scala.collection.immutable.List[scala.Int]
y : example.SampleGeneric[java.lang.String,scala.Long,scala.Int]
c : example.Nested[example.Nested[example.Nested[scala.Int]]]
foo : example.Nested[example.Nested[scala.Int]]
foo : example.Nested[scala.Int]
foo : scala.Int
b : scala.Long
a : java.lang.String
x : scala.Int
これは下記のように使い分けることを目的としたものです。
- 実行時速度が必要ならマクロ
- コンパイル速度が必要ならリフレクション
どちらも同じ TypeDigest
型なので、これを利用している限り、
どちらに依存していても任意に切り替えることができます。
以上
快適な静的型付けライフを!