Reputation: 1346
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.
Upvotes: 0
Views: 84
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
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
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
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