More than 1 year has passed since last update.

これは Scala Advent Calendar 20日目の記事です。
元々は全く別のネタを書こうと思っていたのですが、進捗ダメだった @takashabe がお送りします。

本記事では実行時に与えられた引数によって動的にインスタンスを生成したい欲求に駆られ、Scalaにおける実装例について調べてみた結果についてつらつら書きます。

クラス文字列からインスタンスを生成する

scala.reflect を使って動的にインスタンスを生成することが出来ます。Scalaのリフレクションではミラーと呼ばれるオブジェクトを通してアクセスすることが可能となっています。

※ scala本体にはreflectが含まれていないため、sbtなどで実行する際は依存関係を解決しておく必要があります

build.sbt
libraryDependencies ++= Seq(
  // 言語のバージョンに合わせて
  "org.scala-lang" % "scala-reflect" % "2.11.2",
)

単純なコード例

case class ParameterSchema(name: String, email: String) extends Parameter

class Parameter {
  def generate(className: String, args: Any*): Parameter = {
    // クラスミラーを取得
    val mirror = scala.reflect.runtime.currentMirror
    val classSymbol = mirror.staticClass(className)
    val classMirror = mirror.reflectClass(classSymbol)

    // クラスミラーからコンストラクタミラーを取得
    val constructorSymbol = classSymbol.typeSignature.decl(termNames.CONSTRUCTOR).asMethod
    val constructorMethodMirror = classMirror.reflectConstructor(constructorSymbol)

    // コンストラクタミラーからコンストラクタを実行してインスタンスを生成する
    constructorMethodMirror(args: _*).asInstanceOf[Parameter]
  }
}

val parameter = new Parameter()
val schema = parameter.generate("com.example.root.Parameter", "Alice", "alice@example.com")

println(schema) // => ParameterSchema(Alice,alice@example.com)

コンストラクタが複数ある場合

先の例では ParameterSchema にコンストラクタが1つしか無かったため特に問題ありませんでしたが、補助コンストラクタが用意されていて複数のコンストラクタがある場合は Symbols#asMethod が例外を吐きます。

Symbols#asMethod は1つのコンストラクタが渡されることを期待しています。 asMethod.paramLists で引数の数が取れるので、filter にかけるなりしてやれば良いです。

case class ParameterSchema(name: String, email: String) extends Parameter {
  def this() = this("Bob", "bob@example.com")
}

...

val constructorSymbol = classSymbol.typeSignature.decl(termNames.CONSTRUCTOR).filter {
  _.asMethod.paramLists match {
    case List(Nil)        => // 引数無し
    case List(List(_, _)) => // 引数を2つ取る
  }
}.asMethod

分からなかったこと

元々動的インスタンス生成をやりたかった背景として、CLIからデータを羅列したJSONとJSONをマッピングするためのクラス名を渡して動的にゴニョゴニョしたかったという思いがあります。

そこで以下のような感じで convertTo[T] の部分がミラーなどから指定出来れば幸せになれると思ったのですがよく分かりませんでした...。

val jsonAst = """{ "name":"Alice", "email":"alice@example.com" }""".parseJson
val param = JsonAst.convertTo[ParameterSchema]

もし「そんなことも分からんのかこうすれば動くんじゃ!11」という方がいれば教えて頂ければ幸いです :pray:

まとめ

リフレクションを用いてクラス名の文字列から動的にインスタンスを生成する方法について紹介しました。リフレクションの類なのであまり多用はしない方が良いと思いますが、ここぞという場面では役に立つのではないでしょうか。

参考

http://docs.scala-lang.org/ja/overviews/reflection/overview.html
http://docs.scala-lang.org/ja/overviews/reflection/environment-universes-mirrors.html
http://d.hatena.ne.jp/Kazuhira/20130121/1358780334