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 と呼ばれている記法- 型引数が一つの型をその場で取り出すために使われる
参考リンク