I am trying to get my head around covariance in respect with methods creating new immutable types using lower bounds
class ImmutableArray[+T](item: T, existing:
Consider the followng hierarchy:
class Foo
class Bar extends Foo { def bar = () }
class Baz extends Bar { def baz = () }
And a class similar to yours:
class Cov[+T](val item: T, val existing: List[T] = Nil) {
def append[S >: T](value: S) = new Cov[S](value, item :: existing)
}
Then we can construct three instances for each of the Foo
sub-types:
val cFoo = new Cov(new Foo)
val cBar = new Cov(new Bar)
val cBaz = new Cov(new Baz)
And a test function that requires bar
elements:
def test(c: Cov[Bar]) = c.item.bar
It holds:
test(cFoo) // not possible (otherwise `bar` would produce a problem)
test(cBaz) // ok, since T covariant, Baz <: Bar --> Cov[Baz] <: Cov[Bar]; Baz has bar
Now the append
method, falling back to upper bound:
val cFoo2 = cBar.append(new Foo)
This is ok, because Foo >: Bar
, List[Foo] >: List[Bar]
, Cov[Foo] >: Cov[Bar]
.
Now, correctly your bar
access has gone:
cFoo2.item.bar // bar is not a member of Foo
To understand why you need the upper-bound, imagine the following was possible
class Cov[+T](val item: T, val existing: List[T] = Nil) {
def append(value: T) = new Cov[T](value, item :: existing)
}
class BarCov extends Cov[Bar](new Bar) {
override def append(value: Bar) = {
value.bar // !
super.append(value)
}
}
Then you could write
def test2[T](cov: Cov[T], elem: T): Cov[T] = cov.append(elem)
And the following illegal behaviour would be allowed:
test2[Foo](new BarCov, new Foo) // BarCov <: Cov[Foo]
where value.bar
would be called on a Foo
. Using (correctly) the upper bound, you wouldn't be able to implement append
as in the hypothetical last example:
class BarCov extends Cov[Bar](new Bar) {
override def append[S >: Bar](value: S) = {
value.bar // error: value bar is not a member of type parameter S
super.append(value)
}
}
So the type system remains sound.