Cannot override a type with non-volatile upper bound
Asked Answered
V

3

10


I have a compiler error in scala and I don't know what does it refer to:
Assume these declarations:

trait Abstract {
  type MyType
}
trait AInner
trait A extends Abstract{
  type MyType <: AInner
}
trait BInner {
  def bMethod : Int
}
trait B extends Abstract with A{
  override type MyType <: BInner with A#MyType
}
What I'm trying to achieve here(in trait B) is to further restrict the type MyType declared in Abstract, so any value of type MyType must extend all the MyTypes in the mixin tree.

The compiler is giving me this message(as in title): type MyType is a volatile type; cannot override a type with non-volatile upper bound. I understand, that type volatility is happening here because of type conjuction with A#MyType, the part of the error: type with non-volatile upper bound probably refers to the type declaration type MyType <: AInner, where AInner is not an abstract type thus non-volatile.

Why can't I do it? Is there a way, how to achieve my goal?
Vaenfila answered 8/4, 2013 at 13:31 Comment(0)
T
12

Removing this check in the compiler lets us shine a light on the potential for unsoundness.

diff --git a/src/compiler/scala/tools/nsc/typechecker/Typers.scala b/src/compiler/scala/tools/nsc/typechecker/Typers.scala
index 37a7e3c..78a8959 100644
--- a/src/compiler/scala/tools/nsc/typechecker/Typers.scala
+++ b/src/compiler/scala/tools/nsc/typechecker/Typers.scala
@@ -5128,8 +5128,7 @@ trait Typers extends Adaptations with Tags {

       def typedSelectFromTypeTree(tree: SelectFromTypeTree) = {
         val qual1 = typedType(tree.qualifier, mode)
-        if (qual1.tpe.isVolatile) TypeSelectionFromVolatileTypeError(tree, qual1)
-        else typedSelect(tree, qual1, tree.name)
+        typedSelect(tree, qual1, tree.name)
       }

       def typedTypeBoundsTree(tree: TypeBoundsTree) = {

Then, running the code from a compiler test case for illegal type selection for volatile types:

scala> class A; class B extends A
defined class A
defined class B

scala> trait C {
     |   type U
     |   trait D { type T >: B <: A }
     |   val y: (D with U)#T = new B
     | }
defined trait C

scala> class D extends C {
     |   trait E
     |   trait F { type T = E }
     |   type U = F
     |   def frob(arg : E) : E = arg
     |   frob(y)
     | }
defined class D

scala> new D
java.lang.ClassCastException: B cannot be cast to D$E

As I understand it, the issue stems from the fact that Scala doesn't have true intersection types.

scala> type A = { type T = Int }
defined type alias A

scala> type B = { type T = String }
defined type alias B

scala> "": (A with B)#T
res16: String = ""

scala> 0: (A with B)#T
<console>:37: error: type mismatch;
 found   : Int(0)
 required: String
              0: (A with B)#T
              ^

This might change in the future, if the research into Dependent Object Types (DOT) bears fruit.

Triboelectricity answered 21/4, 2013 at 12:56 Comment(1)
Why are also abstract method members forbidden in non-volatile types?Biblical
N
1

You can rewrite trait B with (more later about your goal, which, I think, is a bit different)

trait B extends A {
  type MyType <: BInner with AInner
}

And this makes total sense. A value of type B#MyType can be seen as either a BInner or a AInner.

You don't need to repeat Abstract because A is already a subclass of Abstract. You don't have to write override as this is implicit for a type declaration. So the question is why A#MyType is not working as AInner?

Here is what the scala language spec says about volatile types.

3.6 Volatile Types

Type volatility approximates the possibility that a type parameter or abstract type instance of a type does not have any non-null values. As explained in (§3.1), a value member of a volatile type cannot appear in a path. A type is volatile if it falls into one of four categories: A compound type T1 with ... with Tn {R } is volatile if one of the following two conditions hold. 1. One of T2, ..., Tn is a type parameter or abstract type, or 2. T1 is an abstract type and and either the refinement R or a type Tj for j > 1 contributes an abstract member to the compound type, or 3. one of T1, ..., Tn is a singleton type. Here, a type S contributes an abstract member to a type T if S contains an abstract member that is also a member of T . A refinement R contributes an abstract member to a type T if R contains an abstract declaration which is also a member of T . A type designator is volatile if it is an alias of a volatile type, or if it designates a type parameter or abstract type that has a volatile type as its upper bound. A singleton type p.type is volatile, if the underlying type of path p is volatile. An existential type T forSome {Q } is volatile if T is volatile.

Other important item mentioned by the spec is about abstract type overriding:

Another restriction applies to abstract type members: An abstract type member with a volatile type (§3.6) as its upper bound may not override an abstract type member which does not have a volatile upper bound.

The compiler error is:

error: overriding type MyType in trait A with bounds <: AInner;
type MyType is a volatile type; cannot override a type with non-volatile upper bound

This is consistent with the spec. BInner with A#MyType is volatile. Before that MyType had a non-volatile as Any.

The matter is that a type in the scala type system must have a unique meaning. An abstract type can be thought as a type which declaration is deferred to a subclass. Therefore there is no problem for declaring values of an abstract type when it is still abstract. On the other hand if we have a type like BInner with A#MyType, this type may have several meaning. It is called volatile and it does not makes sense to have a non null value of this type, as it could have as many types as subclasses instantiating the MyType abstract type. To simplify things, we could think of a volatile type as a type not being a subtype of Any (and volatile as being a subtype Any). We therefore have a contradiction that the compiler mentions.

Coming back to your goal, which you stated as

What I'm trying to achieve here(in trait B) is to further restrict the type MyType declared > in Abstract, so any value of type MyType must extend all the MyTypes in the mixin tree.

You can achieve this thanks to inner traits like this.

trait Abstract {
  type MyType
}
trait B extends Abstract {
  trait MyType {
    def bMethod : Int
  }
}
trait A extends B {
  trait MyType extends super.MyType {
  }
}

Well I hope this somewhat what you're looking for.

Nematode answered 20/4, 2013 at 22:18 Comment(2)
I don't think your interpretation that "because A#MyType is abstract, BInner with A#MyType is bizarre" is correct. It's perfectly valid to write, for example, AInner with Abstract#MyType although it appears equally bizarre. The spec is quite clear about where volatile types may be used, but not as informative about what they mean (that is, why the rules regarding them are necessary for soundness). The only hint there is that they "approximate the possibility that a type does not have any non-null values," and I don't think your argument captures that.Mediocrity
@Mediocrity I reworked the argument and hope it is much clearer now.Nematode
Y
0

What is wrong with this?

trait B extends Abstract with A {
  override type MyType <: BInner with AInner
}

In any realization of trait B, MyType will always be the same type as seen from trait A, so upper-bounding it by itself does not make any sense.

If in the upper piece of code, it bothers you that you'll have to rewrite trait B if you change the bound in trait A, use:

trait A extends Abstract{
  type ABound = AInner
  type MyType <: AInner
}
trait B extends Abstract with A {
  override type MyType <: BInner with ABound
}
Yesteryear answered 19/4, 2013 at 22:26 Comment(1)
That appears to be a good solution, but the thing I still don't understand is why it's necessary. My comment on Leo's answer is the same here... The spec is quite clear about where volatile types may be used, but not as informative about what they mean. The only hint there is that they "approximate the possibility that a type does not have any non-null values," but I'm guessing that's a misleading explanation in this context. Any thoughts?Mediocrity

© 2022 - 2024 — McMap. All rights reserved.