Instantiating a case class with default args via r

2020-04-30 02:54发布

问题:

I need to be able to instantiate various case classes through reflection, both by figuring out the argument types of the constructor, as well as invoking the constructor with all default arguments.

I've come as far as this:

import reflect.runtime.{universe => ru}
val m = ru.runtimeMirror(getClass.getClassLoader)

case class Bar(i: Int = 33)

val tpe      = ru.typeOf[Bar]
val classBar = tpe.typeSymbol.asClass
val cm       = m.reflectClass(classBar)
val ctor     = tpe.declaration(ru.nme.CONSTRUCTOR).asMethod
val ctorm    = cm.reflectConstructor(ctor)

// figuring out arg types
val arg1 = ctor.paramss.head.head
arg1.typeSignature =:= ru.typeOf[Int] // true
// etc.

// instantiating with given args
val p = ctorm(33)

Now the missing part:

val p2 = ctorm()  // IllegalArgumentException: wrong number of arguments

So how can I create p2 with the default arguments of Bar, i.e. what would be Bar() without reflection.

回答1:

Not minimized... and not endorsing...

scala> import scala.reflect.runtime.universe
import scala.reflect.runtime.universe

scala> import scala.reflect.internal.{ Definitions, SymbolTable, StdNames }
import scala.reflect.internal.{Definitions, SymbolTable, StdNames}

scala> val ds = universe.asInstanceOf[Definitions with SymbolTable with StdNames]
ds: scala.reflect.internal.Definitions with scala.reflect.internal.SymbolTable with scala.reflect.internal.StdNames = scala.reflect.runtime.JavaUniverse@52a16a10

scala> val n = ds.newTermName("foo")
n: ds.TermName = foo

scala> ds.nme.defaultGetterName(n,1)
res1: ds.TermName = foo$default$1


回答2:

So in the linked question, the :power REPL uses internal API, which means that defaultGetterName is not available, so we need to construct that from hand. An adoption from @som-snytt 's answer:

def newDefault[A](implicit t: reflect.ClassTag[A]): A = {
  import reflect.runtime.{universe => ru, currentMirror => cm}

  val clazz  = cm.classSymbol(t.runtimeClass)
  val mod    = clazz.companionSymbol.asModule
  val im     = cm.reflect(cm.reflectModule(mod).instance)
  val ts     = im.symbol.typeSignature
  val mApply = ts.member(ru.newTermName("apply")).asMethod
  val syms   = mApply.paramss.flatten
  val args   = syms.zipWithIndex.map { case (p, i) =>
    val mDef = ts.member(ru.newTermName(s"apply$$default$$${i+1}")).asMethod
    im.reflectMethod(mDef)()
  }
  im.reflectMethod(mApply)(args: _*).asInstanceOf[A]
}

case class Foo(bar: Int = 33)

val f = newDefault[Foo]  // ok

Is this really the shortest path?



回答3:

Here's a working version that you can copy into your codebase:

import scala.reflect.api
import scala.reflect.api.{TypeCreator, Universe}
import scala.reflect.runtime.universe._

object Maker {
  val mirror = runtimeMirror(getClass.getClassLoader)

  var makerRunNumber = 1

  def apply[T: TypeTag]: T = {
    val method = typeOf[T].companion.decl(TermName("apply")).asMethod
    val params = method.paramLists.head
    val args = params.map { param =>
      makerRunNumber += 1
      param.info match {
        case t if t <:< typeOf[Enumeration#Value] => chooseEnumValue(convert(t).asInstanceOf[TypeTag[_ <: Enumeration]])
        case t if t =:= typeOf[Int] => makerRunNumber
        case t if t =:= typeOf[Long] => makerRunNumber
        case t if t =:= typeOf[Date] => new Date(Time.now.inMillis)
        case t if t <:< typeOf[Option[_]] => None
        case t if t =:= typeOf[String] && param.name.decodedName.toString.toLowerCase.contains("email") => s"random-$arbitrary@give.asia"
        case t if t =:= typeOf[String] => s"arbitrary-$makerRunNumber"
        case t if t =:= typeOf[Boolean] => false
        case t if t <:< typeOf[Seq[_]] => List.empty
        case t if t <:< typeOf[Map[_, _]] => Map.empty
        // Add more special cases here.
        case t if isCaseClass(t) => apply(convert(t))
        case t => throw new Exception(s"Maker doesn't support generating $t")
      }
    }

    val obj = mirror.reflectModule(typeOf[T].typeSymbol.companion.asModule).instance
    mirror.reflect(obj).reflectMethod(method)(args:_*).asInstanceOf[T]
  }

  def chooseEnumValue[E <: Enumeration: TypeTag]: E#Value = {
    val parentType = typeOf[E].asInstanceOf[TypeRef].pre
    val valuesMethod = parentType.baseType(typeOf[Enumeration].typeSymbol).decl(TermName("values")).asMethod
    val obj = mirror.reflectModule(parentType.termSymbol.asModule).instance

    mirror.reflect(obj).reflectMethod(valuesMethod)().asInstanceOf[E#ValueSet].head
  }

  def convert(tpe: Type): TypeTag[_] = {
    TypeTag.apply(
      runtimeMirror(getClass.getClassLoader),
      new TypeCreator {
        override def apply[U <: Universe with Singleton](m: api.Mirror[U]) = {
          tpe.asInstanceOf[U # Type]
        }
      }
    )
  }

  def isCaseClass(t: Type) = {
    t.companion.decls.exists(_.name.decodedName.toString == "apply") &&
      t.decls.exists(_.name.decodedName.toString == "copy")
  }
}

And, when you want to use it, you can call:

val user = Maker[User]
val user2 = Maker[User].copy(email = "someemail@email.com")

The code above generates arbitrary and unique values. The data aren't exactly randomised. It's best for using in tests.

It works with Enum and nested case class. You can also easily extend it to support some other special types.

Read our full blog post here: https://give.engineering/2018/08/24/instantiate-case-class-with-arbitrary-value.html



回答4:

This is the most complete example how to create case class via reflection with default constructor parameters(Github source):

import scala.reflect.runtime.universe
import scala.reflect.internal.{Definitions, SymbolTable, StdNames}

object Main {
  def newInstanceWithDefaultParameters(className: String): Any = {
    val runtimeMirror: universe.Mirror = universe.runtimeMirror(getClass.getClassLoader)
    val ds = universe.asInstanceOf[Definitions with SymbolTable with StdNames]
    val classSymbol = runtimeMirror.staticClass(className)
    val classMirror = runtimeMirror.reflectClass(classSymbol)
    val moduleSymbol = runtimeMirror.staticModule(className)
    val moduleMirror = runtimeMirror.reflectModule(moduleSymbol)
    val moduleInstanceMirror = runtimeMirror.reflect(moduleMirror.instance)
    val defaultValueMethodSymbols = moduleMirror.symbol.info.members
      .filter(_.name.toString.startsWith(ds.nme.defaultGetterName(ds.newTermName("apply"), 1).toString.dropRight(1)))
      .toSeq
      .reverse
      .map(_.asMethod)
    val defaultValueMethods = defaultValueMethodSymbols.map(moduleInstanceMirror.reflectMethod).toList
    val primaryConstructorMirror = classMirror.reflectConstructor(classSymbol.primaryConstructor.asMethod)
    primaryConstructorMirror.apply(defaultValueMethods.map(_.apply()): _*)
  }

  def main(args: Array[String]): Unit = {
    val instance = newInstanceWithDefaultParameters(classOf[Bar].getName)
    println(instance)
  }
}

case class Bar(i: Int = 33)