LoginSignup
11

リフレクションを使わずに new T を再現する手段 on Scala

Last updated at Posted at 2016-06-07

new T って何さ

こんなやつのことを言ってます

trait Foo {
  def foo: String
}
def execute[T <: Foo] = {
  val instance = new T // これ
  println(instance.foo)
}

型パラメータで指定された型のインスタンスを直接つくりたいというのは、フレームワークやライブラリの開発中に誰しも一度くらい経験があるとおもいます。が、これはもちろん無理です。Java や Scala では型パラメータが type erasure によって消されてしまうためです。

Java であっても Scala であってもリフレクションを駆使することで解決できる場面はなくもないですが、後述するように様々な問題を孕んでいるため、この記事では別なアプローチについて考えます

new T の可否について

本題に入る前に少しおさらいしておきましょう

  • そもそも new T は必要なのか
    • 具象型のファクトリを渡せばいい話だろう
      • しかしそれを毎回実装しなければいけないというのも…
  • new T は安全に使えるのだろうか
    • デフォルトコンストラクタがない場合はどうなる
      • 不完全なオブジェクトを作って再代入しまくるか
      • それとも実行時にクラッシュか
  • リフレクションの問題もいくつかあるよね
    • スレッドセーフじゃないし実行速度も不安
      • つまり避けられるなら避けたい

この辺りの議論についてはググるとたくさん出てきますが、まとめるとこんな感じです

  • new T の需要はある
  • とはいえ危険性があるのも確か

つまり : 型パラメータからインスタンスを生成する安全な方法がほしい!

この問題はシンプルな trait によって解決できます。
そう、Scala ならね

trait HasConstructor[A]{
  def newInstance: A
}
object HasConstructor {
  type HasDefaultConstructor[A] = HasConstructor[() => A]
}
def execute[A <: Foo: HasDefaultConstructor]() = {
  val instance = implicitly[HasDefaultConstructor[A]].newInstance()
  println(instance.foo)
}

HasConstructor という trait を導入しました。これは文字通り「型 A にはコンストラクタがある」ことをコンパイラに示すための型です。execute メソッドにはその他に一切余計な制限が足されていない点に注目してください。HasConstructor[() => A] の implicit 定義さえ用意されていればどんな Foo 型も渡せます。

この execute の呼び出し側のコードは :

class CustomFoo extends Foo {
  override def foo = "hello!"
}
object CustomFoo {
  implicit object constructor extends HasConstructor[() => CustomFoo]{
    def newInstance = () => new CustomFoo
  }
}
execute[CustomFoo]()// hello!
execute[Foo]()// compile error

残念ながらあまり綺麗ではありませんね…。
とはいえこの時点で目的の要件は全て守れています

  • CustomFoo という型パラメータを指定するだけで呼び出せている
    • execute の利用者は明示的に new しなくていい
  • new できない型が渡されてもコンパイル時に検出できている
  • リフレクションも全く使っていない

ここでは implicit 定義によってインスタンスの生成方法を記述していますが、CustomFoo のコンストラクタに引数がないことは見ればわかります。どうにかしてそれをコンパイラに知らせることはできないものでしょうか

つまり : implicit の実装を毎回手作業で書きたくない!

この問題は数十行のマクロによって解決できます。
そう、Scala ならね(しつこい)

object HasConstructor {
  implicit def reify[A]: HasConstructor[A] = macro ReificationImpl.infer[A]
  type HasDefaultConstructor[A] = HasConstructor[() => A]
}
private object ReificationImpl {
  def infer[A: c.WeakTypeTag](c: blackbox.Context): c.Tree = {
    val factory = new ReificationFactory { override val context: c.type = c }
    val tree = factory createConstructor c.weakTypeOf[A]
    // println(tree)
    tree
  }
}
trait ReificationFactory {
  val context: blackbox.Context
  import context.universe._

  def createConstructor(target: Type): Tree = {
    if (!target.typeSymbol.fullName.startsWith("scala.Function")){
      val name = typeOf[HasConstructor[_]].typeSymbol.name.encodedName
      val message = s"FunctionN must be applied to $name. current: $target"
      throw new IllegalArgumentException(message)
    }
    val newInstance = target.typeArgs match {
      case xs :+ last =>
        val args = xs map { x =>
          val arg = TermName(context freshName "x")
          q"$arg: $x"
        }
        q"(..$args) => new $last(..$args)"
      case x =>
        throw new IllegalArgumentException(s"unknown type: $x")
    }
    val constructor = appliedType(
      typeOf[HasConstructor[_]].typeConstructor,
      target
    )
    q"""new $constructor {
      override def newInstance = $newInstance
    }"""
  }
}

object HasConstructor にマクロを呼び出すための implicit def を追加しました。これによって呼び出し側のコードから退屈な implicit 実装を消すことができます

class CustomFoo extends Foo {
  override def foo = "hello!"
}
execute[CustomFoo]()// hello!
execute[Foo]()// compile error

明示的に new を書くだけの簡単なお仕事はもう必要ありません。もちろん前項で挙げていた要件も全て守られています。

一件落着!…ではありますが、せっかくなのでもう少し一般化してみます

引数が必要なコンストラクタにも対応したい!

聡明な読者の方々はお気づきかもしれません。ここまでの type HasDefaultConstructor はあくまで HasConstructor[() => A] のエイリアスであり、これは「引数 0 個で A を生成できる」という意味でした。今度は「引数 1 個から A を生成できる」パターンについても試してみましょう

class CustomFoo2(arg: String) extends Foo {
  override def foo = s"Hello, $arg!"
}
def execute2[A <: Foo](arg: String)(implicit i: HasConstructor[String => A]) = {
  val instance = i.newInstance(arg)
  println(instance.foo)
}
execute2[CustomFoo2]("World")// Hello, World!
execute2[CustomFoo]("boooo")// compile error

実は HasConstructor はこのままで問題なく動いてくれます。

execute2 の定義に足されている [String => A] によって「コンストラクタが String 一つを受け取れるような Foo なら何でもいいよ」という情報がマクロに伝わっているためです。CustomFoo には String を受け取るようなコンストラクタは存在しないので無事にコンパイル時にエラーになります。

ちなみに execute のときと同じ記法で execute2 の implicit 引数を消すこともできますが…

def execute2[A <: Foo: ({ type L[X] = HasConstructor[String => X] })#L](arg: String) = {
  val instance = implicitly[HasConstructor[String => A]].newInstance(arg)
  println(instance.foo)
}

お世辞にも読みやすくはありません。このあたりは現在の Scala の限界ですね

まとめ

new T の抱える問題は implicit によって解決できる

  • 型安全にインスタンスを生成できる
  • リフレクションを使わないのでスレッドセーフ、速度も心配なし
  • リフレクションに頼れない Android のような環境でも問題なし

ヒント

用語について

  • 上記 HasConstructor は型クラス
    • 今回のマクロではこの型クラスのインスタンスを生成している
  • ({ type L[X] = ... })#L は type lambda と呼ばれている記法
    • 型引数が一つの型をその場で取り出すために使われる

参考リンク

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11