So say we have a few classes like this:
abstract class Throw {
def winsOver(t2: Throw): Boolean
}
class Rock extends Throw {
override def winsOver(t2: Throw): Boolean = t2 match {
case _: Scissors => true
case _ => false
}
}
class Scissors extends Throw {
override def winsOver(t2: Throw): Boolean = t2 match {
case _: Paper => true
case _ => false
}
}
class Paper extends Throw {
override def winsOver(t2: Throw): Boolean = t2 match {
case _: Rock => true
case _ => false
}
}
This works
scala>new Paper winsOver new Rock
res0: Boolean = true
scala>new Rock winsOver new Rock
res1: Boolean = false
The code has a bunch of repetition, however. Since the only thing that varies is the type that they beat, we could try to factor that out
abstract class Throw {
type Beats <: Throw
def winsOver(t2: Throw): Boolean = t2 match {
case _: Beats => true
case _ => false
}
}
class Rock {
type Beats = Scissors
}
class Scissors {
type Beats = Paper
}
class Paper {
type Beats = Rock
}
But then the compiler starts complaining
warning: abstract type pattern Throw.this.Beats is unchecked since it is eliminated by erasure
case _: Beats => true
And sure enough, it doesn't work. winsOver
suddenly always returns true
scala>new Rock winsOver new Rock
res0: Boolean = true
I've been trying to figure this out and from what I've found, this is because the JVM doesn't carry around as much type information as it could. This leads to some information being lost ("erasure") and there are ways to get around this in scala, previously with manifests and now with classtags and typetags.
I haven't really been able to figure out more concretely how this works, and while I have sometimes been able to copy code snippets from the Internet to do similar things, I don't really understand how that code works and I can't adapt it to this example. I've also noticed that there is the shapeless library which has a lot of support for this kind of thing, but I would also like to understand how it works myself.
You should not check type information at runtime. Type erasure is a good thing and Scala should erase more types than it does.
Instead, use algebraic data types and pattern matching:
sealed abstract class Throw {
def winsOver(t2: Throw): Boolean
}
case object Rock extends Throw {
def winsOver(t2: Throw): Boolean = t2 match {
case Scissors => true
case _ => false
}
}
case object Scissors extends Throw {
def winsOver(t2: Throw): Boolean = t2 match {
case Paper => true
case _ => false
}
}
case object Paper extends Throw {
def winsOver(t2: Throw): Boolean = t2 match {
case Rock => true
case _ => false
}
}
This has some repetition, so we can factor it out:
sealed abstract class Throw {
def winsOver(t2: Throw): Boolean = (this,t2) match {
case (Paper, Rock) | (Rock, Scissors) | (Scissors,Paper) => true
case _ => false
}
}
case object Rock extends Throw
case object Scissors extends Throw
case object Paper extends Throw
This works as expected:
scala> Rock winsOver Scissors
res0: Boolean = true
scala> Paper winsOver Scissors
res1: Boolean = false