0

(Scala 2.12.8) Full Example

So lets say you have some "TypeEvidence" for some specific concrete types:

sealed trait TypeEvidence[+T]
object TypeEvidence{
  case object DoubleType extends TypeEvidence[Double]
  case object LongType extends TypeEvidence[Long]
  case object StringType extends TypeEvidence[String]
}

I can match on those evidence objects like this:

object ThisIsOk{
  def apply[T](ev: TypeEvidence[T]): Option[T] = {
    ev match {
      case TypeEvidence.DoubleType => Some(123.456)
      case TypeEvidence.LongType => Some(1234L)
      case _ => None
    }
  }
}

But not like this:


class ThisFails[T]{
  def apply(ev: TypeEvidence[T]): Option[T] = {
    ev match {
      case TypeEvidence.DoubleType => Some(123.456)
      case TypeEvidence.LongType => Some(1234L)
      case _ => None
    }
  }
}

This fails to compile with:

pattern type is incompatible with expected type;
 found   : TypeEvidence.DoubleType.type
 required: TypeEvidence[T]
pattern type is incompatible with expected type;
 found   : TypeEvidence.LongType.type
 required: TypeEvidence[T]

Why is this? And how can this be worked around?

Seth Tisue
  • 29,985
  • 11
  • 82
  • 149
  • 4
    fwiw, it compiles in Scala 3.2.0. In general, Scala 3 is a lot better at GADTs than Scala 2 is. (I don't know what specific improvements might be in play here.) – Seth Tisue Sep 08 '22 at 19:09
  • 1
    https://stackoverflow.com/questions/40544070 https://stackoverflow.com/questions/20359696 https://stackoverflow.com/questions/17072185 https://github.com/scala/bug/issues/5195 – Dmytro Mitin Sep 08 '22 at 19:51
  • We know. Unfortunately progressing to scala 3 is not an option at this time. – memorableusername Sep 08 '22 at 19:52

2 Answers2

0

What if you do this with a type class?

class ThisFails[T]{
  def apply(ev: TypeEvidence[T])(implicit 
    teto: TypeEvidenceToOption[T]
  ): Option[T] = teto(ev)
}

trait TypeEvidenceToOption[T] {
  def apply(ev: TypeEvidence[T]): Option[T]
}
object TypeEvidenceToOption {
  implicit val double: TypeEvidenceToOption[Double] = 
    { case TypeEvidence.DoubleType => Some(123.456) }
  implicit val long: TypeEvidenceToOption[Long] = 
    { case TypeEvidence.LongType => Some(1234L) }
  implicit def default[T]: TypeEvidenceToOption[T] = _ => None
}

new ThisFails[Double].apply(TypeEvidence.DoubleType) // Some(123.456)
new ThisFails[Long].apply(TypeEvidence.LongType) // Some(1234)
new ThisFails[String].apply(TypeEvidence.StringType) // None
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
0

I'm not entirely sure why it didn't work the way you expected. The only reasonable explanation I tell to myself is that the compiler has no issue using T as a placeholder for any type while T belongs to the parameterized method, whereas when it belongs to a parameterized class, it's more restrictive in terms of what you can do with it.

As a workaround, you can use .type and .asIstanceOf:

  class ThisFails[T] {
    def apply(ev: TypeEvidence[T]): Option[T] = {
      ev match {
        case _: TypeEvidence.DoubleType.type => Some(123.456.asInstanceOf[T])
        case _: TypeEvidence.LongType.type   => Some(1234L.asInstanceOf[T])
        case _                               => None
      }
    }
  }

  val x  = new ThisFails[Long]
  val x2 = new ThisFails[Double]
  val x3 = new ThisFails[String]

  println(x.apply(TypeEvidence.LongType))    // Some(1234)
  println(x2.apply(TypeEvidence.DoubleType)) // Some(123.456)
  println(x3.apply(TypeEvidence.StringType)) // None

I'm aware it's not nice, but you asked for a workaround. This pattern match now matches an object of the given type. Of course, with this workaround, having the objects "caseable" is redundant now.

I had a peek at the disassembled code of object ThisIsOk to figure out this workaround:

public static class ThisIsOk$
{
    public static final ThisIsOk$ MODULE$;
    
    static {
        ThisIsOk$.MODULE$ = new ThisIsOk$();
    }

    public <T> Option<T> apply(final Main.TypeEvidence<T> ev) {
        Object module$;
        if (Main.TypeEvidence$.DoubleType$.MODULE$.equals(ev)) {
            module$ = new Some((Object)BoxesRunTime.boxToDouble(123.456));
        }
        else if (Main.TypeEvidence$.LongType$.MODULE$.equals(ev)) {
            module$ = new Some((Object)BoxesRunTime.boxToLong(1234L));
        }
        else {
            module$ = None$.MODULE$;
        }
        return (Option<T>)module$;
    }
}

I saw here the way pattern match works is by calling equals on their types, but since equals is not overridden, what this actually does is a reference check. Recall:

// in Object.java file:
public boolean equals(Object obj) {
    return (this == obj);
}

Also I saw a cast to Option<T> in the end. So with these observations, the class ThisFails[T] workaround I just showed earlier gets disassembled into this:

public static class ThisFails<T>
{
    public Option<T> apply(final Main.TypeEvidence<T> ev) {
        Object module$;
        if (Main.TypeEvidence$.DoubleType$.MODULE$ == ev) {
            module$ = new Some((Object)BoxesRunTime.boxToDouble(123.456));
        }
        else if (Main.TypeEvidence$.LongType$.MODULE$ == ev) {
            module$ = new Some((Object)BoxesRunTime.boxToLong(1234L));
        }
        else {
            module$ = None$.MODULE$;
        }
        return (Option<T>)module$;
    }
}

Which is almost identical to the object ThisIsOk's disassembled code, except for using == instead of equals, which does not matter in this case, as I explained earlier.

I tested it with both Scala 2.13.8 and your version here on Scastie, and both worked.

Alin Gabriel Arhip
  • 2,568
  • 1
  • 14
  • 24