scalatypeserasure

Checking scala types at runtime and type erasure


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.


Solution

  • 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