ScalaDay 11

trait からフィールドの型情報を取り出すためのライブラリを作った

More than 3 years have passed since last update.

( 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.fooInt 型であるということが分かります。

念のため試してみましょう。

> ~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 型なので、これを利用している限り、

どちらに依存していても任意に切り替えることができます。


以上

快適な静的型付けライフを!