scalatypecheckingscala-generics

Weird failure of Scala's type-check


My application dictates need of an argument provider trait that can be added to any class to allow passing of arbitrary number of arguments of any type with it.

trait Arg
case class NamedArg(key: String, value: Any) extends Arg

// I extend my classes with this trait
trait ArgsProvider {
  val args: Seq[Arg]

  lazy val namedArgs: Map[String, Any] = {
    args.filter(_.isInstanceOf[NamedArg]).
      map(_.asInstanceOf[NamedArg]).
      map(arg => arg.key -> arg.value).toMap
  }

  ...
}

Then I can extract the NamedArgs from args of ArgsProvider using their key as follows

trait ArgsProvider {
  ...

  /*
   * Method that takes in a [T: ClassTag] and a (key: String) argument
   * (i) if key exists in namedArgs: Map[String, Any]
   *     - Returns Some(value: T) if value can be casted into T type
   *     - Throws Exception if value can't be casted into T type
   * (ii) if key doesn't exist in namedArgs
   *    Returns None
   */
  def getOptionalTypedArg[T: ClassTag](key: String): Option[T] = {
    namedArgs.get(key).map { arg: Any =>
      try {
        arg.asInstanceOf[T]
      } catch {
        case _: Throwable => throw new Exception(key)
      }
    }
  }
  ...
}

Even though it may seem highly unintuitive and verbose, this design works flawlessly for me. However, while writing certain unit tests, I recently discovered a major loophole in it: it fails to perform type-checking. (or at least that's what I infer)


To be more specific, it doesn't throw any exception when I try to type-cast the provided arg into wrong type. For example:

// here (args: Seq[NamedArg]) overrides the (args: Seq[Arg]) member of ArgsProvider
case class DummyArgsProvider(args: Seq[NamedArg]) extends ArgsProvider

// instantiate a new DummyArgsProvider with a single NamedArg having a String payload (value)
val dummyArgsProvider: DummyArgsProvider = DummyArgsProvider(Seq(
    NamedArg("key-string-arg", "value-string-arg")
))

// try to read the String-valued argument as Long
val optLong: Option[Long] = dummyArgsProvider.getOptionalTypedArg[Long]("key-string-arg")

While one would expect the above piece of code to throw an Exception; to my dismay, it works perfectly fine and returns the following output (on Scala REPL)

optLong: Option[Long] = Some(value-string-arg)


My questions are:


I'm using


Solution

  • As @AlexeyRomanov has remarked, as/isInstanceOf[T] don't use a ClassTag.

    You can use pattern matching instead, which does check with a ClassTag, if there's one available:

    trait ArgsProvider {
      /* ... */
    
      def getOptionalTypedArg[T: ClassTag](key: String): Option[T] = {
        namedArgs.get(key).map {
          case arg: T => arg
          case _ => throw new Exception(key)
        }
      }
    }
    

    Or you can use methods of ClassTag directly:

    import scala.reflect.classTag
    
    def getOptionalTypedArg[T: ClassTag](key: String): Option[T] = {
      namedArgs.get(key).map { arg =>
        classTag[T].unapply(arg).getOrElse(throw new Exception(key))
      }
    }