5

I am researching about existential types in Scala 2.12.x. For that I'm testing the following code:

trait Parent
class ChildA extends Parent
class ChildB extends Parent

def whatIsInside(opt: Option[_ <: Parent]): String = {
  opt match {
    case _: Option[_ <: ChildA] => "ChildA"
    case _: Option[_ <: ChildB] => "ChildB"
    case _                      => throw new IllegalArgumentException("unknown type")
  }
}


whatIsInside(Some(new ChildA))

I don't expect this to work at runtime because of type erasure, but nevertheless this does not even compile. I am getting the following error:

[error] ExistentialTypes.scala:12:24: not found: type _$2
[error]         case _: Option[_ <: ChildA] => "ChildA"
[error]                        ^
[error] ExistentialTypes.scala:13:24: not found: type _$3
[error]         case _: Option[_ <: ChildB] => "ChildB"
[error]                        ^

Can someone explain these errors?

  • 1
    "I don't expect this to work at runtime because of type erasure, but nevertheless this does not even compile." - That's a fairly strange approach to compilation. The job of the compiler frontend is *exactly that*: rejecting programs at compile time that would break at runtime. – Andrey Tyukin May 22 '21 at 16:43
  • @AndreyTyukin that's dummy code. I don't intend for that to work, just to learn why it doesn't – Nicolas Schejtman May 22 '21 at 16:47
  • 3
    Is your goal to make it work, or is your goal really to research the limitations of the existential types in previous versions of the scala compiler? If the latter is the case, then one should probably tag this question with "scalac" / "scala-compiler". – Andrey Tyukin May 22 '21 at 16:55
  • @AndreyTyukin exactly, the latter is the case. I will take your suggestion. Thank you! – Nicolas Schejtman May 22 '21 at 19:49

1 Answers1

4

(Not a full answer, but a few notes and links; Maybe it can serve as a starting point for someone else)

In 2.12.13, the compiler seems to be able to prove that F[_ <: X] and F[X] are the same type if X occurs in covariant position:

println(implicitly[Option[_ <: ChildA] =:= Option[ChildA]])

This compiles (with warnings, but it compiles):

trait Parent
class ChildA extends Parent
class ChildB extends Parent

def whatIsInside(opt: Option[_ <: Parent]): String = {
  opt match {
    case _: Option[ChildA] => "ChildA"
    case _: Option[ChildB] => "ChildB"
    case None            => "None"
    case _ => throw new Error("meh")
  }
}

This does not compile:

trait Parent
class ChildA extends Parent
class ChildB extends Parent

def whatIsInside(opt: Option[_ <: Parent]): String = {
  opt match {
    case _: Option[_ <: ChildA] => "ChildA"
    case _: Option[_ <: ChildB] => "ChildB"
    case None            => "None"
    case _ => throw new Error("meh")
  }
}

So, it seems that it must have something to do with the bounds inference for the synthetic _$2 type-variable.

Also, this compiles:

def testConforms[A >: Nothing <: ChildA](ca: Option[A]): Option[_ <: Parent] = ca

so, unless I'm misinterpreting the spec:

If there exists a substitution σ over the type variables a_i,…,a_n such that σT conforms to pt, one determines the weakest subtype constraints C1 over the type variables a1,…,an such that C0 ∧ C1 implies that T conforms to [the expected type] pt.

, the pt would be Option[_ <: Parent], then a1,...,an would be the single synthetic type _$2, and the constraints _$2 >: Nothing <: ChildA should make the type Option[_$2] conform to Option[_ <: Parent]. So, it seems that it should work, but doesn't.


Bonus

If you just wanted to make it work, then just skip all those wildcards, they aren't needed:

trait Parent
class ChildA extends Parent
class ChildB extends Parent

def whatIsInside(opt: Option[Parent]): String = {
  opt match {
    case Some(_: ChildA) => "ChildA"
    case Some(_: ChildB) => "ChildB"
    case None            => "None"
    case _ => throw new Error("meh")
  }
}


whatIsInside(Some(new ChildA))
whatIsInside(Some(new ChildB))
whatIsInside(None)
Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • 2
    @MarioGalic I don't think it's something "syntactic". The `F` in your example does not introduce any type-variable binders, whereas the `Option[_ <: ChildA]` does introduce a synthetic type-variable `_$2` for the `_`, but then fails to infer any reasonable type bounds for it. I think that something breaks in the bound-inference for the type variable. – Andrey Tyukin May 22 '21 at 18:00
  • Thank you very much Andrey for your answer. I am reflecting on what you said. I'm not so fluent interpreting the Scala spec but so I let you know what I am thinking and you can tell me if it makes sense to you. – Nicolas Schejtman May 22 '21 at 20:05
  • The compilers type inference is able to workout the synthetic type from the `opt` parameter because it can provide a substitution from the actual type supplied as argument for the method. The synthetic types in the pattern branches however have to be "worked out" from other type patterns instead of actual types, so the compiler does not find any suitable substitution because it has no "actual concrete input", just another pattern – Nicolas Schejtman May 22 '21 at 20:08