ScalaでもリフレクションでtoStringしたい

はじめに

Scalaを始めたのでとりあえずリフレクションでtoStringしてみました。 弊社にとってリフレクションでtoStringするのはもはや挨拶のようなものです。

環境

Scalaのバージョンは2.12.3です。

真のジェネリック

当たり前かどうかは分かりませんが、JVMの型パラメータはコンパイル時に消去されてしまい実行時に取れません。 この辺の問題は『真のジェネリック問題』として2012年をピークに激しく議論されていたようですが、流れ弾はいやなのでとりあえずスルーします。

当然JVMの民*1であるScalaもこの制約を受けます。その為、CLRの民であるVBのように

Sub Hoge(Of T)(obj As T)
    Dim t As Type = GetType(T)
End Sub

のようにGetType(T)で一発で型情報を取得してアビャーするわけにはいかないようです。

そこで、Scalaはコンテキストバインドという形で型に関する情報をコンパイル時に必要なメソッド等に引き渡すコードを生成し、そこで型情報をやり取りするようです。

仮に

def reflectToString[T: TypeTag : ClassTag](value: T): Unit

のようなシグネチャを持つメソッドは以下のようにコンパイルされます。

> javap -private .\Hello.class

Compiled from "Hello.scala"
public final class example.Hello {
  public static <T> void reflectToString(T, scala.reflect.api.TypeTags$TypeTag<T>, scala.reflect.ClassTag<T>);
  public static void main(java.lang.String[]);
}

本来の引数の後ろに型情報に関するパラメータを追加し、コンパイラコンパイル時に型情報を暗黙的に追加するようです。

package example

import scala.reflect._
import scala.reflect.runtime.universe._

class Hoge(val name: String)

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

object Hello {
  def main(args: Array[String]): Unit = {
    val p = new Person("Hoge", 10)
    reflectToString(p)
    println()
    val h = new Hoge("HogeHoge")
    reflectToString(h)
  }

  def reflectToString[T: TypeTag : ClassTag](value: T): Unit = {
    val fs: Iterable[TermSymbol] = typeOf[T]
      .decls
      .filter(_.isTerm)
      .filter(!_.isMethod)
      .map(_.asTerm)
    val rm: Mirror = typeTag[T].mirror
    val im: InstanceMirror = rm.reflect(value)

    for (it <- fs) {
      val fm: FieldMirror = im.reflectField(it)
      println(s"${it.name}= ${fm.get}")
    }
  }
}
name = Hoge
age = 10

name = HogeHoge

TypeTagとってTermSymbolとってRuntimeMirrorとってInstanceMirrorとってFieldMirrorとってgetするのは.NETのリフレクションに慣れた身からするとなんか複雑に感じますが、まぁしゃーない。

おわり

*1:Scala.jsというのもあるようですが