How can I create an instance of a Case Class with constructor arguments with no Parameters in Scala?
Asked Answered
S

4

7

I'm making a Scala app that sets by reflection field values. This works OK.

However, in order to set field values I need a created instance. If I have a class with an empty constructor, I can do this easily with classOf[Person].getConstructors....

However, when I try doing this with a Case class with a non empty constructor It doesn't work. I have all of the field names and its values, as well as the Object type I need to create. Can I instance the Case Class somehow with what I've got?

The only thing I don't have is the parameter names from the Case Class constructor or a way to create this without parameter and then setting the values via reflection.

Let's go to the example.

I have the following

case class Person(name : String, age : Int)
class Dog(name : String) {
    def this() = {
        name = "Tony"
    }
}

class Reflector[O](obj : O) {

    def setValue[F](propName : String, value : F) = ...

    def getValue(propName : String) = ...
}

//This works
val dog = classOf[Dog].newInstance()
new Reflector(dog).setValue("name", "Doggy")

//This doesn't
val person = classOf[Person].newInstance //Doesn't work

val ctor = classOf[Person].getConstructors()(0)
val ctor.newInstance(parameters) //I have the property names and values, but I don't know 
// which of them is for each parameter, nor I name the name of the constructor parameters
Slipstream answered 11/12, 2012 at 1:21 Comment(0)
F
5

If you are looking for a way to instantiate the object with no arguments, you could do the same as you did in your example, just so long as your reflection setter can handle setting the immutable vals.

You would provide an alternate constructor, as below:

case class Person(name : String, age : Int) {
    def this() = this("", 0)
}

Note that the case class will not generate a zero-arg companion object, so you will need to instantiate it as: new Person() or classOf[Person].newInstance(). However, that should be what you are looking to do.

Should give you output like:

scala> case class Person(name : String, age : Int) {
     |         def this() = this("", 0)
     |     }
defined class Person

scala> classOf[Person].newInstance()
res3: Person = Person(,0)
Fluidextract answered 11/12, 2012 at 2:29 Comment(5)
Thanks!. However, I know that, but I don't want the user of this to have to create this default constructor with default values that won't ever be valid. Do you know of another way?Slipstream
@mgonto, if the variables are in the class they will either be null or have some default value. You could always set name to null, but an int will need some type of default value since it can't be null. You could use reflection to try to call the constructor appropriately, but I would think you would need to know a lot of things about what is getting passed in ahead of time.Fluidextract
For example in this case, which would be the default value for name and age? There would be none. I'd actually add a require(name != null) in the code. If I did that and I'd supply null as the constructor parameter, I'd got an exception.Slipstream
So, you would say that in order to use this, you would make users of this "framework" create a default constructor for all types they'd want to create this with?Slipstream
It all depends, and I am not entirely sure what your usage scenario is. In your example, you set the default value of name to Tony, which was why I supplied the empty string. I don't think it is atypical for a framework to require that a zero-arg constructor also be implemented.Fluidextract
M
4

The case class should have default args, so that you can just Person(); in the absence of a default arg, supplying a null for name might (or ought to) hit a require(name != null).

Alternatively, use reflection to figure out which params have defaults and then supply nulls or zeros for the rest.

import reflect._
import scala.reflect.runtime.{ currentMirror => cm }
import scala.reflect.runtime.universe._

// case class instance with default args

// Persons entering this site must be 18 or older, so assume that
case class Person(name: String, age: Int = 18) {
  require(age >= 18)
}

object Test extends App {

  // Person may have some default args, or not.
  // normally, must Person(name = "Guy")
  // we will Person(null, 18)
  def newCase[A]()(implicit t: ClassTag[A]): A = {
    val claas = cm classSymbol t.runtimeClass
    val modul = claas.companionSymbol.asModule
    val im = cm reflect (cm reflectModule modul).instance
    defaut[A](im, "apply")
  }

  def defaut[A](im: InstanceMirror, name: String): A = {
    val at = newTermName(name)
    val ts = im.symbol.typeSignature
    val method = (ts member at).asMethod

    // either defarg or default val for type of p
    def valueFor(p: Symbol, i: Int): Any = {
      val defarg = ts member newTermName(s"$name$$default$$${i+1}")
      if (defarg != NoSymbol) {
        println(s"default $defarg")
        (im reflectMethod defarg.asMethod)()
      } else {
        println(s"def val for $p")
        p.typeSignature match {
          case t if t =:= typeOf[String] => null
          case t if t =:= typeOf[Int]    => 0
          case x                        => throw new IllegalArgumentException(x.toString)
        }
      }
    }
    val args = (for (ps <- method.paramss; p <- ps) yield p).zipWithIndex map (p => valueFor(p._1,p._2))
    (im reflectMethod method)(args: _*).asInstanceOf[A]
  }

  assert(Person(name = null) == newCase[Person]())
}
Mavis answered 11/12, 2012 at 3:3 Comment(4)
I've tried the default for Case Classes. Even though I add a default for a certain class, if I get the constructor through reflection, I get the constructor with all parameters. And I only have the type, I cannot call the apply method manually but through reflection. And if I call this via reflection, I still get all of the parameters, it doesn't check for Default valuesSlipstream
If I supply null for a parameter for the constructor, if then it's assumed that the field is not null or if there's a require as you say, it might hit an exception, so I don't think this is the complete solution. Do you have another idea? Thanks!Slipstream
The defaults for <init> args are on the companion object. I was just about to code that for example; I forget everytime exactly how, so it's a useful exercise. My idea was just, use those defaults if available (named default$foo$1 or whatever), otherwise try null or zero.Mavis
BTW, the other way to subvert a ctor is serialization. I don't think I've seen that exploited for DI. I guess if you have a prototype object, it doesn't matter much if it's in memory or serialized.Mavis
W
2

The approach below works for any Scala class that has either a no-arg ctor or has an all defaulted primary ctor.

It makes fewer assumptions than some others about how much information is available at the point of call as all it needs is a Class[_] instance and not implicits etc. Also the approach does not rely on the class having to be a case class or having a companion at all.

FYI During construction precedence is given to the no-arg ctor if present.

object ClassUtil {

def newInstance(cz: Class[_ <: AnyRef]): AnyRef = {

    val bestCtor = findNoArgOrPrimaryCtor(cz)
    val defaultValues = getCtorDefaultArgs(cz, bestCtor)

    bestCtor.newInstance(defaultValues: _*).asInstanceOf[A]
  }

  private def defaultValueInitFieldName(i: Int): String = s"$$lessinit$$greater$$default$$${i + 1}"

  private def findNoArgOrPrimaryCtor(cz: Class[_]): Constructor[_] = {
    val ctors = cz.getConstructors.sortBy(_.getParameterTypes.size)

    if (ctors.head.getParameterTypes.size == 0) {
      // use no arg ctor
      ctors.head
    } else {
      // use primary ctor
      ctors.reverse.head
    }
  }

  private def getCtorDefaultArgs(cz: Class[_], ctor: Constructor[_]): Array[AnyRef] = {

    val defaultValueMethodNames = ctor.getParameterTypes.zipWithIndex.map {
      valIndex => defaultValueInitFieldName(valIndex._2)
    }

    try {
      defaultValueMethodNames.map(cz.getMethod(_).invoke(null))
    } catch {
      case ex: NoSuchMethodException =>
        throw new InstantiationException(s"$cz must have a no arg constructor or all args must be defaulted")
    }
  }
}
Worried answered 7/7, 2014 at 0:24 Comment(0)
G
0

I ran into a similar problem. Given the ease of using Macro Paradise, Macro Annotations are a solution (for scala 2.10.X and 2.11 so far).

Check out this question and the example project linked in the comments below.

Guernsey answered 1/4, 2014 at 3:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.