Reflect type name in String to actual type in Scal

2019-08-11 09:09发布

I've the following class-object

class DefaultValue[+A](val default: A)

object DefaultValue {

  implicit object DefaultDouble extends DefaultValue[Double](-1.0)
  implicit object DefaultFloat extends DefaultValue[Float](-1f)
  implicit object DefaultInt extends DefaultValue[Int](-1)
  implicit object DefaultBoolean extends DefaultValue[Boolean](false)

  def value[A](implicit value: DefaultValue[A]): A = value.default
}

To use this I call DefaultValue.value[Int] or DefaultValue.value[Double] etc

However, I need the ability to call DefaultValue.value[Int] when I have user input as "Int" : the typename written in String, and similarly for others.

I wish to do this without pattern-matching, as it would essentially beat the purpose of inserting TypeTag in the function. Is there a better way to do this, perhaps using Scala Reflection?

1条回答
疯言疯语
2楼-- · 2019-08-11 09:41

One problem with that is that you won't be able to get a good return type. Any or DefaultValue[_] is the best you can get.

You can use reflection to write something like this:

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

def defaultByTypename(typename: String): DefaultValue[_] = {
  val defaultValueName = "Default" + typename.trim.capitalize
  val objectSymbol = 
    ru.typeOf[DefaultValue.type].member(ru.TermName(defaultValueName))
  cm.reflectModule(objectSymbol.asModule).instance.asInstanceOf[DefaultValue[_]]
}

This will work for implicit objects. If you want it to work with implicit vals (e.g. implicit val DefaultString = new DefaultValue[String]("~")), you'd have to add code for this case:

def defaultByTypename(typename: String): DefaultValue[_] = {
  val defaultValueName = "Default" + typename.trim.capitalize
  val objectSymbol = 
    ru.typeOf[DefaultValue.type].member(ru.TermName(defaultValueName))
  val result = if (objectSymbol.isModule) {
    cm.reflectModule(objectSymbol.asModule).instance
  } else if (objectSymbol.isTerm) {
    cm.reflect(DefaultValue).reflectField(objectSymbol.asTerm).get
  } else sys.error("Unknown typename")
  result.asInstanceOf[DefaultValue[_]]
}

But actually, I think, it's not a bad idea to go for a Map with the correspondence:

val typename2DefaultValue = Map[String, DefaultValue[_]](
  "Double" -> DefaultDouble,
  "Float" -> DefaultFloat,
  "Int" -> DefaultInt,
  "Boolean" -> DefaultBoolean
)

This approach is probably faster and really not that hard to maintain. Also, it would be possible to not require direct correspondence between object name and user input, or to have several possible strings for a single DefaultValue, etc.


Also, if you declare the base class as sealed, and extend it only with objects, you could streamline the creation of this Map:

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

sealed class DefaultValue[+A](val default: A)

object DefaultValue {
  implicit object DefaultDouble extends DefaultValue[Double](-1.0)
  implicit object DefaultFloat extends DefaultValue[Float](-1f)
  implicit object DefaultInt extends DefaultValue[Int](-1)
  implicit object DefaultBoolean extends DefaultValue[Boolean](false)

  val typename2DefaultValue: Map[String, DefaultValue[_]] = {
    val subclasses = ru.symbolOf[DefaultValue[_]].asClass.knownDirectSubclasses
    subclasses.map { subclass =>
      subclass.name.toString.stripPrefix("Default") ->
        cm.reflectModule(cm.staticModule(subclass.fullName)).instance.asInstanceOf[DefaultValue[_]]
    }.toMap
  }
}
查看更多
登录 后发表回答