Subtyping and Generics (Bounds and Covariance) - rohgar/scala-principles-1 GitHub Wiki
As seen previously, Scala exhibits polymorphism using:
- Subtyping: Instances of subclass can be passed in place of a base class.
- Generics: Classes or functions can accept generic type parameters.
Here we will look at their interaction:
- Bounds
- Variance
We have the class hierarchy Any > AnyRef > IntSet > Empty and NonEmpty
Consider the method assertAllPositives which
- takes an
IntSet - returns the
IntSetitself if all its elements are positive - throws an exception otherwise
What would be the best type you can give to assertAllPos? Maybe:
def assertAllPositives(s: IntSet): IntSetIn most situations this is fine, but can one be more precise?
Here if assertAllPositives takes Empty set, it returns Empty sets and if it takes NonEmpty sets it returns NonEmpty sets. The above definition does not express this. A way to express this is using bounds:
def assertAllPositives[S <: IntSet](r: S): S = ...Here, <: IntSet is an upper bound of the type parameter S: It means that S can be instantiated only to types that conform to IntSet.
-
[S <: T]means: S is a subtype of T.
Eg. If T is IntSet, then S could be one of Empty, NonEmpty or IntSet.
-
[S >: T]means: S is a supertype of T, or T is a subtype of S.
Eg. If T is NonEmpty, then S could be one of NonEmpty, IntSet, AnyRef, or Any.
-
[S >: T2 <: T1]means: s is any type on interval between T1 and T2.
Eg. if T2 is NonEmpty and T1 is IntSet, then S is any type on the interval between NonEmpty and `IntSet.
There’s another interaction between subtyping and type parameters we need to consider.
Given:
NonEmpty <: IntSetis
List[NonEmpty] <: List[IntSet] ?Intuitively, this makes sense: A list of non-empty sets is a special case of a list of arbitrary sets.
We call types for which this relationship holds covariant because their subtyping relationship varies with the type parameter. Thus Lists in scala are covariant.
Does covariance make sense for all types, not just for List? No. In Scala Arrays are not Covariant.
In Java, Arrays are Covariant i.e. NonEmpty[] <: IntSet[].
But this causes problems. Consider the Java code:
NonEmpty [] a = new NonEmpty []{ new NonEmpty (1 , Empty , Empty )}
IntSet[] b = a
b[0] = Empty
NonEmpty s = a [0]It looks like we assigned in the last line an Empty set to a variable of type NonEmpty! The 3rd line would give an ArrayStoreException as Java stores a Type-tag for the Array against which it checks at Runtime.
Question: So when does it make sense to subtype one type with another?
Answer: The Liskov Substitution Principle: If A <: B, then everything one can to do with a value of
type B one should also be able to do with a value of type A.
The above array example will be written in Scala as follows:
val a: Array[NonEmpty] = Array(new NonEmpty(1, Empty, Empty))
val b: Array[IntSet] = a
b(0) = Empty
val s: NonEmpty = a(0)Scala gives a type error in line 2, since in Scala Arrays are not covariant.
Lists are immutable and Arrays are mutable. So types which are mutable should not be covariant.
Say C[T] is a parameterized type, and A, B are types such that A <: B.
There are 3 possible relationships between C[A] and C[B]:
// Given
A <: B
// then:
C[A] <: C[B] // C is covariant
C[A] >: C[B] // C is contravariant
neither C[A] or C[B] is a subtype of the other // C is nonvariantScala lets you declare the variance of a type by annotating the type parameter:
class C[+A]
class C[-A]
class C[A]
So if
type A = IntSet => NonEmpty
type B = NonEmpty => IntSetAccording to the Liskov Principle, A <: B - since B can return an Empty or NonEmpty, but A can return only NonEmpty.
So in general, functions are contravariant in argument types and covariant in return types:
// if
A2 <: A1 and B1 <: B2
// then
A1 => B1 <: A2 => B2trait List[T] {
def isEmpty: Boolean
def head: T
def tail: List[T]
}
class Cons[T](val head: T, val tail: List[T]) extends List[T] {
def isEmpty = false
}
class Nil extends List[String] {
def isEmpty: Boolean = true
def head: T = throw new NoSuchElementException()
def tail: List[T] = throw new NoSuchElementException()
}Can be changed to:
trait List[+T] {
...
}
class Nil extends List[Nothing] { //Nothing type is at the bottom of Scala’s type hierarchy
...
}
//usage
val v : List[String] = NilReference: http://stackoverflow.com/questions/4531455/whats-the-difference-between-ab-and-b-in-scala
Q[A <: B] means that class Q can take any class A that is a subclass of B.
Q[+B] means that Q can take any class, but if A is a subclass of B, then Q[A] is considered to be a subclass of Q[B].
Q[-B] means that Q can take any class, but if A is a subclass of B, then Q[B] is considered to be a subclass of Q[A].
class Animal {}
class Dog extends Animal {}
class Poodle extends Dog {}
val animalsList: MYList[Animal] = new MYList(animal, dog) //polymorphism (parent class reference is used to refer to a child class object.)
val dogsList: MYList[Dog] = new MYList(dog)- Here
MYListis nonvariant.
class MYList[T](elements: T*) {}
def func1(list: MYList[Animal]) = {}
def func2(list: MYList[Dog]) = {}
func1(animalsList) // GOOD
func1(dogsList) // FAILS
func2(animalsList) // FAILS
func2(dogsList) // GOOD- Here
MYListis covariant.
class MYList[+T](elements: T*) {}
def func1(list: MYList[Animal]) = {}
def func2(list: MYList[Dog]) = {}
func1(animalsList) // GOOD
func1(dogsList) // GOOD - covariance
func2(animalsList) // FAILS
func2(dogsList) // GOOD- Here
MYListis contra-variant.
class MYList[+T](elements: T*) {}
def func1(list: MYList[Animal]) = {}
def func2(list: MYList[Dog]) = {}
func1(animalsList) // GOOD
func1(dogsList) // FAILS
func2(animalsList) // GOOD - Contravariance
func2(dogsList) // GOODThis shows that functions are contravariant in argument types and covariant in return types.
package com.example.playground
class Car {}
class SportsCar extends Car {}
class Ferrari extends SportsCar {}
object morecovariance extends App {
// Test 1: Works as expected
def test1( arg: SportsCar => SportsCar ) = {
new SportsCar
}
def foo1(arg: Car): Ferrari = { new Ferrari }
def foo2(arg: SportsCar): Car = { new Ferrari }
def foo3(arg: Ferrari): Ferrari = { new Ferrari }
test1(foo1) // compiles
test1(foo2) // Fails due to wrong return type.
test1(foo3) // Fails due to wrong parameter type
}