简体   繁体   中英

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.

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

The simple solution would be

import scala.reflect.ClassTag

abstract class Throw[Beats <: Throw : ClassTag] {
    def winsOver(t2: Throw): Boolean = t2 match {
        case _: Beats => true
        case _        => false
    }
}

class Rock extends Throw[Scissors]
...

: ClassTag is a context bound which means an implicit ClassTag[Beats] is available, and : Beats pattern has special support for this case.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM