1

Why does the following code work when Foo is invariant, but not when it is covariant? The covariant version of Foo produces a type error saying that in the call of useF1, the argument has type Foo[T] but F1 is required. A similar error is produced for useF2.

If the variance annotation is removed from Foo, the code works. The pattern match against F1 exposes the fact that T = Int, so that x has type Foo[Int]. The implicit conversion function is used to convert Foo[Int] to F1 in the argument of useF1. Similarly for F2. What part of this process is different when Foo is covariant, and why?

// A GADT with two constructors
sealed abstract class Foo[+T]
final case class F1() extends Foo[Int]
final case class F2() extends Foo[Unit]

object Example {
  // A Foo[Int] can only be an F1
  implicit def refineGADT(x : Foo[Int]) : F1 = x.asInstanceOf[F1]
  // A Foo[Unit] can only be an F2
  implicit def refineGADT(x : Foo[Unit]) : F2 = x.asInstanceOf[F2]

  def useF1(x : F1) = ()
  def useF2(x : F2) = ()

  def demo[T](x : Foo[T]) = x match {
    case F1() => useF1(x) // error
    case F2() => useF2(x) // error
  }
}

Although GADTs make subtyping more complicated in general, in this case the only two possible concrete types are Foo[Int] and Foo[Unit], and no subtyping relationship holds between them, so subtyping shouldn't affect this example.

Heatsink
  • 7,721
  • 1
  • 25
  • 36

2 Answers2

0

You just have to rebind the matched F1 or F2 in demo:

def demo[T](x : Foo[T]) = x match {
  case y@F1() => useF1(y)
  case y@F2() => useF2(y)
}

x is of type Foo[T] so the compiler can't deduce that it is valid input for useF1. When T is invariant, the compiler can deduce a little bit more and is able to resolve this case. When T is covariant, we know that it is valid in the matched case, but we have to bind a new identifier to help the compiler. You can even bind the matched classes to x to shadow the outer x (e.g. case x@F1()), but then you lose the ability to reference the outer x if you need to.

Ben Reich
  • 16,222
  • 2
  • 38
  • 59
  • That avoids the type error. I am still hoping someone will explain why the variance annotation prevents type inference for these function calls, though. – Heatsink Apr 03 '15 at 05:22
0

First, let's simplify your example (assuming we ignore type erasure):

class Foo[+T] 

def demo[T](x : Foo[T]) = x match {
  case _: Foo[Int] => x: Foo[Int] //error but works with `class Foo[T]`
  case _: Foo[Unit] => x: Foo[Unit] //error but works with `class Foo[T]`
}

Or even:

class Foo[T]
scala> def demo[T](x : Foo[T]) = x match {case _: Foo[Int] => x}
demo: [T](x: Foo[T])Foo[Int] //notice Int

class Foo[+T]
scala> def demo[T](x : Foo[T]) = x match {case _: Foo[Int] => x}
demo: [T](x: Foo[T])Foo[T] //notice T

Expected type of x as expression is the existential type Foo[_ >: T] (as a result of covariance applied to the return type), or more precisely Foo[X >: T] forSome{type X}. So compiler can't process it as this feature or bug (typecast in context of matching) doesn't work for existential types as it can't prove that Foo[Int] always belongs to R, where R :> Foo[X] for some X >: Int. So R may be :> Foo[Int] or may be :> Foo[Any] or something else :> Foo[_ :> Int], which makes R a coproduct of possible ranges. Such coproduct can't be cast to Foo[Int]:

class Foo[T] 

def demo(x : Foo[_]) = x match {
  case _: Foo[Int] => x: Foo[Int] //error
  case _: Foo[Unit] => x: Foo[Unit] //error 
}                 

<console>:9: error: type mismatch;
 found   : Foo[_$1] where type _$1
 required: Foo[Int]
             case a: Foo[Int] => x: Foo[Int] //error
                                 ^
                        ^

<console>:10: error: type mismatch;
 found   : Foo[_$1] where type _$1
 required: Foo[Unit]
             case a: Foo[Unit] => x: Foo[Unit] //error
                                  ^

P.S. Example about how covariance relates to existentiality:

scala> class Foo[T]
defined class Foo

scala> def demo[T](x : Foo[T]) = (x: Foo[_ >: Int]) //can't cast to something in Int..Any range
<console>:17: error: type mismatch;
 found   : Foo[T]
 required: Foo[_ >: Int]
Note: T <: Any, but class Foo is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)
       def demo[T](x : Foo[T]) = (x: Foo[_ >: Int])
                                  ^

scala> class Foo[+T]
defined class Foo

scala> def demo[T](x : Foo[T]) = (x: Foo[_ >: Int])
demo: [T](x: Foo[T])Foo[Any]
dk14
  • 22,206
  • 4
  • 51
  • 88
  • I'm not following how `x : Foo[T]` becomes `x : Foo[_ >: T]`. If I try to apply the definition of covariance I get the opposite bound. Covariance means `S <: T` implies `F[S] <: F[T]`. Therefore, `F[T]` includes `F[S]` for any subtype `S` of `T`, or `x : Foo[_ <: T]`. Your last example shows it's possible to cast to a less precise type `Foo[U]` where `T <: U`. The types `Foo[_ >: Int]` and `Foo[Int]` behave differently: the result of `new Foo[Unit]` can be held in `x` if `x` has the former type, but not the latter. – Heatsink Apr 07 '15 at 17:04
  • `x : Foo[_ <: T]` - it depends from which side you're looking at. When you're looking at the input `demo(x : Foo[T])` - `x` can be anything less than `Foo[T]`. When you're looking at the required output - it's possible to cast `x` to anything bigger than `Foo[T]`. Without covariance this "bigger" may be `Foo[T]..Any`. With covariance there is "double" range `Foo[T]..Any`, where `T` may be one of (some of) `T..Any`, so the return type of `case _: Foo[Int] => x` is expected to be `>: Foo[T] forSome {type T <: Any} = Foo[_ >: T]`. – dk14 Apr 07 '15 at 17:55
  • So compiler has to check that `Foo[Int]` always belongs to `R`, where `R :> Foo[X]` for some `X >: Int` - so `R` may be `:> Foo[Int]` or may be `:> Foo[Any]` or something else `:> Foo[_ :> Int]`, which makes `R` a coproduct of possible ranges. Without covariance it has to check that `Foo[Int]` always belongs to `R` where `R :> Foo[Int]` – dk14 Apr 07 '15 at 19:42