douglaz
douglaz

Reputation: 1346

Working around a type erasure match in Scala

This is the sample code:

object GenericsTest extends App {
  sealed trait CommonResponse
  trait Requester[SomeType] {
    trait Response extends CommonResponse {
      def entity: SomeType
    }

    case class SomeResponse(entity: SomeType) extends Response
    // More Responses here...
    def getResponse(): Response
  }

  object StringRequester extends Requester[String] {
    override def getResponse(): StringRequester.Response = SomeResponse("somestring")
  }

  object IntegerRequester extends Requester[Integer] {
    override def getResponse(): IntegerRequester.Response = SomeResponse(42)
  }

  val response: CommonResponse = IntegerRequester.getResponse()
  response match {
    case r: StringRequester.Response => println(s"Got string response ${r.entity}") // hits here =(
    case r: IntegerRequester.Response => println(s"Got integer response ${r.entity}")
    case other => println(s"Got other $other")
  }
}

It prints "Got string response 42" instead of "Got integer response 42"

The real code is a service that implements the indexing of different types and will return different Responses if the data is already indexed or not, etc.

  1. Is there some workaround?
  2. How to make the compiler warn about this particular situation?

Upvotes: 0

Views: 84

Answers (4)

Oleg Pyzhcov
Oleg Pyzhcov

Reputation: 7353

As I stated in the comment, replacing trait Response with abstract class Response fixes the issue in Scala 2.12. This will result in capturing $outer pointer and its being checked in the pattern match (you can see it by using -Xprint:jvm compiler parameter.

I was not able to find this change specified in release notes for 2.12.0 so it might be not intentional. Highly suggesting covering that with a unit test.

Upvotes: 1

Ra Ka
Ra Ka

Reputation: 3055

Scala erase generic types at runtime. For example, runtime type of List[String] and List[Integer] is same. Therefore, your code is not working.

For example,

  sealed trait Foo
  case class Bar[T](value: T) extends Foo

  val f1: Foo = Bar[String]("Apple")
  val f2: Foo = Bar[Integer](12)

  //Will print ---A even type of f2 is Integer. 
  f2 match {
    case x: Bar[String]=> println("--- A")
    case x: Bar[Integer] => println("--- B")
    case _ => println("---")
  }

The above will print ---A, because in scala, type of generics are erased at runtime, therefore, compiler wont know about type of f2.

In your case, you have defined 2 instance of Requester. StringRequester and IntegerRequester. The type of response of StringRequest is Requester[String]#Response and IntegerRequester is Requester[String]#Response. Here, Response is a path dependent type, i.e. type of Response is different with different instance. For example, StringRequester1.Response is not equal to StringRequester2.Response.

However, due to the generic, the above condition will fail. Because, due to type erasure in generic, the type SomeType is removed at runtime from Requester.

i.e. Requester[String]#Response will be Requester[_]#Response
and Requester[Integer]#Response will be Requester[_]#Response

StringRequester.getResponse().isInstanceOf[IntegerRequester.Response] //will print true.
//because both type of StringRequester.getResponse() and IntegerRequest.Response is Requester[_]#Response.

As a result, both type are equal. Because of this your code fails to give proper result.

  response match {
    case r: StringRequester.Response => println(s"Got string response ${r.entity}") // hits here =(
    case r: IntegerRequester.Response => println(s"Got integer response ${r.entity}")
    case other => println(s"Got other $other")
  }

In above code, in both case type of r is Requester[_]#Response at runtime, hence both will match, and Scala match the first found case i.e. StringRequester.Response. If you swap the place as below, it will print integer.

  response match {
    case r: IntegerRequester.Response => println(s"Got integer response ${r.entity}")
    case r: StringRequester.Response => println(s"Got string response ${r.entity}") // hits here =(
    case other => println(s"Got other $other")
  }

Below is the workaround: You can use reflection type check as below.

  sealed trait Foo
  case class Bar[T : TypeTag](value: T) extends Foo {
    def typeCheck[U: TypeTag] = typeOf[T] =:= typeOf[U]
  }

  val f1:Foo = Bar[String]("apple")
  val f2:Foo = Bar[Integer](12)

  // Will print Integer type.
  f2 match {
    case x: Bar[String] if x.typeCheck[String] => println("Value is string type.")
    case x: Bar[Integer] if x.typeCheck[Integer] => println("Value is Integer type.")
    case _ => println("--- none ---")
  }

Upvotes: 1

chengpohi
chengpohi

Reputation: 14227

Since the inner trait Response doesn't have the implementation outer method for Path dependent type checking in the runtime, this is caused that trait is actually an interface in Java

so for your example, you should use SomeResponse for type matching, example:

  response match {
    case r: StringRequester.SomeResponse => println(s"Got string response ${r.entity}") // hits here =(
    case r: IntegerRequester.SomeResponse => println(s"Got integer response ${r.entity}")
    case _ => println(s"Got other")
  }

the SomeResponse own the outer method for typing checking in runtime. see:

  public Test$Requester Test$Requester$SomeResponse$$$outer();
    Code:
       0: aload_0
       1: getfield      #96                 // Field $outer:LTest$Requester;
       4: areturn

Upvotes: 1

HTNW
HTNW

Reputation: 29193

You can expose a reference to the outer Requester from within the response:

trait Requester[SomeType] {
  trait Response {
    def requester: Requester.this.type = Requester.this
    // ...
  }
  // ...
}

You can then match on response.requester:

response.requester match {
  case StringRequester => println(s"Got string response ${response.entity}") // hits here =(
  case IntegerRequester => println(s"Got integer response ${response.entity}")
  case _ => println(s"Got other $response")
}

Upvotes: 1

Related Questions