1

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:

  • Why is type-check failing here?
  • Under what circumstances does Scala's type-checking fail, in general?
  • Can this design be improved?

I'm using

  • Scala 2.11.11
  • SBT 1.0.3
y2k-shubham
  • 10,183
  • 11
  • 55
  • 131
  • you should try to find a simplified example. This is really long to read. Also, by trying the narrow the problem, you might simply find the error yourself – Juh_ Feb 20 '18 at 09:38
  • Thanks **@Juh_**, I'll try to come up with a *simpler example* and, in due course, narrow down the problem. In the meantime, I would appreciate if you could tell me **whether you are able to reproduce the problem** (just *copy-paste* the given code in `Scala REPL`). I can confirm that even outside my `IntelliJ` project, I'm able to replicate this behaviour on `Scala 2.12.4 REPL` – y2k-shubham Feb 20 '18 at 09:50

2 Answers2

3

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))
  }
}
Kolmar
  • 14,086
  • 1
  • 22
  • 25
1

You're having problem with type-erasure: The Option[Long] actually store the String "value-string-arg" and doesn't care about its type which as been erased.

However, if you do optLong.get it will then try to cast it to a Long which is the expected output. And you'll get the ClassCastException


Just a little comments:

replace

val namedArgs: Map[String, Any] = {...}

by

val namedArgs: Map[String, Any] = args.collect{
  case NameArg(k, v) => k -> v
}(collection.breakout)

Also, in your getOptionalTypedArg, don't catch all Throwable. It is bad practice (you could catch OutOfMemoryError and other Fatal errors, which you should not). In your case, you want to catch a ClassCastException. In other case where you don't know exactly which Throwable, try using NonFatal

Juh_
  • 14,628
  • 8
  • 59
  • 92
  • Thanks for your valuable inputs **@Juh_**. I can see that due to `type-erasure`, an `Option[Long]` can refer to `Some(String)`. But don't you think `arg.asInstanceOf[T]` should be able to withstand `erasure` [since it's an **explicit type-cast** analogous to `Java`'s `NewType objOfNewType = (NewType) objOfOldType;`]? Correct me if I'm wrong, but if `erasure` can downplay (manual) `type-casting` then wouldn't `[T: ClassTag]` be rendered pretty-much useless? (which, of course, isn't true; `ClassTag` comes from `scala.reflect` package) – y2k-shubham Feb 20 '18 at 10:19
  • 2
    `as/isInstanceOf[T]` don't use a `ClassTag` even if one is available. To cast it you need to work with `ClassTag`'s methods explicitly. It _is_ exactly analogous to `(NewType) objOfOldType`: `(T) objOfOldType` behaves the same and for the same reason. – Alexey Romanov Feb 20 '18 at 10:24
  • I don't fully understand it either, but asInstanceOf[T] cannot be executed due to type-erasure. Thus, the cast is done when the output of getOptionalTypedArg is accessed. Read this https://stackoverflow.com/a/6690611/1206998 – Juh_ Feb 20 '18 at 13:04