VARIANCE IN SCALA LYLE KOPNICKY JUNE 16, 2015
VARIANCE IN SCALALYLE KOPNICKY
JUNE 16, 2015
OVERVIEWAlbum/Track exampleCovarianceSubtypingContravarianceInvariance
MUSIC COLLECTIONclass Album(val title: String, val tracks: Vector[Track])
class Track(val title: String, val length: Int)
DIFFERENT KINDS OF ALBUMSclass VinylAlbum(val title: String, val tracks: Vector[Track], val rpm: Int) extends Album
class MP3Album(val title: String, val tracks: Vector[Track], val bitrate: Int) extends Album
DIFFERENT KINDS OF TRACKStrait Track { val title: String val length: Int}
class VinylTrack(val title: String, val length: Int, val widthInMM: Int) extends Track
class MP3Track(val title: String, val length: Int, val path: String) extends Track
TYING TRACK SUBTYPES TO ALBUM SUBTYPEStrait Album { val title: String val tracks: Vector[Track]}
class VinylAlbum(val title: String, val tracks: Vector[VinylTrack], val rpm: Int) extends Album
class MP3Album(val title: String, val tracks: Vector[MP3Track], val bitrate: Int) extends Album
THIS IS ALLOWEDval vt1 = new VinylTrack("4′33″", 273, 5)val vt2 = new VinylTrack("0′00″", 0, 0)
val va = new VinylAlbum("Greatest Hits", Vector(vt1, vt2), 33)
val mt1 = new MP3Track("4′33″", 273, "/Music/John Cage/Greatest Hits/4′33″.mp3")val mt2 = new MP3Track("0′00″", 0, "/Music/John Cage/Greatest Hits/0′00″.mp3")
val ma = new MP3Album("Greatest Hits", Vector(mt1, mt2), 256)
THIS IS NOT ALLOWEDval vt1 = new VinylTrack("4′33″", 273, 5)val vt2 = new VinylTrack("0′00″", 0, 0)
val ma = new MP3Album("Greatest Hits", Vector(vt1, vt2), 256)
error: type mismatch; found : this.VinylTrack required: this.MP3Trackval ma = new MP3Album("Greatest Hits", Vector(vt1, vt2), 256) ̂
WHY DOES THIS WORK?1. Album is covariant with respect to each of its val fields2. Vector is covariant with respect to its type parameter
WHAT'S COVARIANCE?A relationship between compound types:
Given types ,and some compound type ,if ,
A <: BF[A]
F[A] <: F[B]then we say that is covariant in .F A
WHY IS THIS VALID?A value of a subtype can be used where a value of thesupertype is expectedThe properties of a Vector are such that the subtypingrelationship carries over from the parameterVectors are statically declared to be covariant
WHAT MAKES A SUBTYPE?Imagine a context where a value of type is expectedIf you can always use a value of type in that context,Then .
BA
A <: B
WHO DECIDES WHETHER YOU CAN?Languages may define subtyping rulesWithout such a rule, there is no subtypingIn statically-typed languages, subtype relationships mustbe declared
WIDTH SUBTYPING OF RECORDSThe subtype has additional fields:
trait Track(val: title, val length: Int)class VinylTrack(val title: String, val length: Int, val widthInMM: Int) extends Track
It makes sense to use a VinylTrack where a track is expected,because you can perform all the Track operations on it.
DEPTH SUBTYPING OF RECORDSThe subtype has fields that are subtypes of the correspondingfields in the supertype:
class TrackRating(val track: Track, val rating: Int)class VinylTrackRating(val track: VinylTrack, val rating: Int) extends TrackRating
The extends is required to statically declare therelationship.
YOU CAN'T DO THISclass TrackRating(val ratedThing: Track, val rating: Int)class AlbumRating(val ratedThing: Album, val rating: Int) extends TrackRating
Because Album is not a subtype of Track.
PROPERTIES OF A VECTORImmutableCan add or modify elements to produce a new Vector
VECTOR SUBTYPINGIf context expects a Vector[Track],it expects to be able get an element and treat it like aTrackso it's OK if that's really a VinylTrack
VECTOR SUBTYPINGIt also expects to be able to append a Vector[Track]:
v1 : Vector[VinylTrack]v2 : Vector[Track]v1 ++ v2 : Vector[Track]
VECTOR SUBTYPINGThus Vector[VinylTrack] Vector[Track]<:
It's also declared as covariant in the type: Vector[+A]
ALBUM SUBTYPINGCombined width & depth subtyping:
trait Album { val title: String val tracks: Vector[Track]}
class VinylAlbum(val title: String, val tracks: Vector[VinylTrack], val rpm: Int) extends Album
ALBUM WITH A TYPE PARAMETERYou could instead turn the Track type into a constrainedparameter:
trait Album[+T <: Track] { val title: String val tracks: Vector[T]}
class VinylAlbum(val title: String, val tracks: Vector[VinylTrack], val rpm: Int) extends Album[VinylTrack]
This works essentially the same, but creates additionalintermediate types, such as Album[VinylTrack].
CONTRAVARIANCEThe reverse of covarianceIf , then Commonly occurs in function parameters
A <: B F[B] <: F[A]
FUNCTION SUBTYPINGtype Predicate[A] = A -> Boolean
def spinsFast(album: VinylAlbum): Boolean = { rpm > 33 }
def isBigAlbum(album: Album): Boolean = { tracks.length > 10 }
Given , Is Predicate[A] Predicate[B]?A <: B <:
FUNCTION SUBTYPINGContext expects a Predicate[Album]. Can we provide aPredicate[VinylAlbum], such as spinsFast?
No, because the context might apply it to an MP3Album,which has no rpm field.
FUNCTION SUBTYPINGContext expects a Predicate[VinylAlbum]. Can weprovide a Predicate[Album], such as isBigAlbum?
Yes, because even an MP3Album has a number of tracks, asdo all Albums.
FUNCTION SUBTYPINGSo, when , Predicate[B] Predicate[A].A <: B <:
We say that Predicate is contravariant in its typeparameter.
We should declare it as
type Predicate[-A] = A -> Boolean
VARIANCE IN FUNCTIONSIn general, the function type is contravariant in andcovariant in .
A → B AB
A type that is contravariant in one paramter and covariant inthe other is called provariant. Map[-A, +B] is anotherexample.
For multiple input parameters, function types arecontravariant in all parameter types.
VARIANCE IN METHODSMethods are like functions, but also have a self type.You might think of self as a hidden parameter, andtherefore contravariant.But methods are covariant in the self type because it isused to dispatch to the appropriate subclass.In languages with multiple dispatch, all parameters used fordispatch are covariant.
COVARIANT AND CONTRAVARIANT POSITIONSEvery class/trait declaration has covariant and contravariantpositions for types:
trait Album { val title: String val tracks: Vector[Track] def playOn(player: Player): Unit def isBig: Boolean}
vals (title, tracks) are in covariant positionmethod arguments (player) are in contravariant positionmethod return types are in covariant position
COVARIANT AND CONTRAVARIANT POSITIONSSo, we couldn't use subtypes of Player in a subtype, right?
class VinylPlayer(...) extends Player
class VinylAlbum(val title: String, val tracks: Vector[VinylTrack]) { def playOn(player: VinylPlayer): Unit { ... }}
Turns out you can, but it's a separate method overloading
COVARIANT AND CONTRAVARIANT POSITIONSWhen creating a subtype, the types in each position arechecked against the supertype, to satisfy the variance rules.
Type parameters must simultaneously meet the criteria forall positions where they are used.
COVARIANT AND CONTRAVARIANT POSITIONSTry the track type as a parameter:
trait Album[+T <: Track] { val title: String val tracks: Vector[T] def isLongerThan(otherAlbum: Album[T]): Boolean}
Illegal: Parameter is declared covariant but used in acontravariant position
INVARIANCEWe can use the type parameter in both positions if we give upcovariance:
trait Album[T <: Track] { val title: String val tracks: Vector[T] def isLongerThan(otherAlbum: Album[T]): Boolean}
Then Album[VinylTrack] is neither a subtype nor asupertype of Album[Track].
INVARIANCEBut this is OK:
trait Album { val title: String val tracks: Vector[Track] def isLongerThan(otherAlbum: Album): Boolean}
VinylAlbum.isLongerThan must also accept all Albums
INVARIANCEAll var types are invariant positions, since they serve as boththe return type of a getter and the parameter type of a setter.
trait Album { val title: String var tracks: Vector[Track]}
This would make Album invariant in Vector[Track], sosubtypes couldn't specialize.
INVARIANCEThinking of fields as getters/setters:
trait Album { def title: String def tracks: Vector[Track] def tracks=(Vector[Track]): Unit}
Since Vector[Track] appears in both covariant andcontravariant positions, subtypes can't specialize.
INVARIANCE OF MUTABLE TYPESLike a var, Array[T] must be invariant in T. Think of how itwould go wrong if Array were covariant:
val vt1: VinylTrack = ...val mta = Array.empty: Array[MP3Track]mta(0) = vt1 # BAD
You can't use an Array[MP3Track] in place of anArray[Track].
INVARIANCE OF MUTABLE TYPESNow think of how it would go wrong if Array werecontravariant:
val vt1: VinylTrack = ...val ta = Array(vt1): Array[Track]val mt = ta(0): MP3Track # BADval p = mt.path
You can't use an Array[Track] in place of anArray[MP3Track].
KEY POINTSWhen using extends, component types are checkedThose in covariant position must be subtypesThose in contravariant position must be supertypesType parameters can be declared as covariant orcontravariantThese declarations will be checked against positionsParameters in both positions must be invariant
OTHER TOPICSNested levels of contravariance flipping each other(function of function)Binary methodsUseful overloading