Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

scala's type checker doesn't recognize types in abstract path-dependent classes scenario

Tags:

types

scala

Let's define a trait with an abstract class

object Outer {

  trait X {
    type T
    val empty: T
  }

Now we can make an instance of it:

  val x = new X {
    type T = Int
    val empty = 42
  }

Scala now recognizes, that x.empty is an Int:

  def xEmptyIsInt = x.empty: Int

Now, let's define an other class

  case class Y(x: X) extends X {
    type T = x.T
    val empty = x.empty
  }

and make an instance of it

  val y = Y(x)

But now Scala, isn't able to infer that y.empty is of type Int. The following

  def yEmptyIsInt = y.empty: Int

now produces an error message:

error: type mismatch;
found   : y.x.T
required: Int
       y.empty : Int

Why is this the case? Is this a scala bug?

We can mitigate this issue by introducing a parameters to the case class:

  case class Z[U](x: X { type T = U }) extends X {
    type T = U
    val empty = x.empty
  }

Then it works again

  val z = Z(x)
  def zEmptyIsInt: Int = z.empty

But we always have to mention all the types inside X at call-site. This ideally should be an implementation detail which leads to the following approach:

  case class A[U <: X](z: U) extends X {
    type T = z.T
    val empty = z.empty
  }

This also mitigates the issue

  val a = A(x)
  def aEmptyIsInt: Int = a.empty

}

So, to summarize, my questions are the following: Why does the simple case doesn't work? Is this a valid workaround? What other problems might come up when we follow one of the two workaround approaches? Is there a better approach?

like image 638
Fabian Schmitthenner Avatar asked Nov 09 '22 14:11

Fabian Schmitthenner


1 Answers

You've re-used x for different things, so from here on I'll call the object instantiated by val x "instance x" and the x: X used in class Y "parameter x".

"Instance x" is an anonymous subclass of trait X with concrete members overriding trait X's abstract members. As a subclass of X you can pass it to the constructor for case class Y and it will be accepted happily, since as a subclass it is an X.

It seems to me you expect that case class Y will then check at runtime to see if the instance of X it is passed has overridden X's members, and generate an instance of Y whose members have different types depending on what was passed in.

This is emphatically not how Scala works, and would pretty much defeat the purpose of its (static) type system. For example, you wouldn't be able to do anything useful with Y.empty without runtime reflection since it could have any type at all, and at that point you're better off just using a dynamic type system. If you want the benefits of parametricity, free theorems, not needing reflection, etc. then you have to stick to statically determined types (with a small exception for pattern matching). And that's what Scala does here.

What actually happens is that you've told Y's constructor that parameter x is an X, and so the compiler statically determines that x.empty, has the type of X.empty, which is abstract type T. Scala's inference isn't failing; your types are actually mismatched.

Separately, this doesn't really have anything to do with path-dependent types. Here is a good walkthrough, but in short, path-dependent types are bound to their parent's instance, not determined dynamically at runtime.

like image 193
David Prichard Avatar answered Nov 14 '22 22:11

David Prichard