Skip to content

Instantly share code, notes, and snippets.

@stumash
Last active April 8, 2020 05:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stumash/6defbea2586b9d9b96c1a96115b3ed57 to your computer and use it in GitHub Desktop.
Save stumash/6defbea2586b9d9b96c1a96115b3ed57 to your computer and use it in GitHub Desktop.
scala covariance and contravariance
import scala.collection.mutable.ArrayDeque
object Co_and_Contra_Variance {
def main(args: Array[String]): Unit = {
theBasicIdea() // - What covariance and contravariance let you do, without discussing why or how
theCoreConceptIsBasedOnFunctions() // - How do we decide if function f1 can be substituted by f2? (aka f2 is subtype of f1)
bringItAllTogether() // - Now that we know why and how, let's see how it applies
}
def theBasicIdea(): Unit = {
class A()
class B() extends A
class C() extends B
// This class is covariant (+T) with respect to its generic type parameter T, so you
// can use instances of MyCovariantBox[SubclassOfT] wherever a MyCovariantBox[T] is required
class MyCovariantBox[+T]()
def useMyCovariantBox(covbox: MyCovariantBox[B]): Unit = println("got a box of B (or sub B)")
useMyCovariantBox(new MyCovariantBox[A]()) // ERROR: type mismatch: expected MyCovariantBox[B]
useMyCovariantBox(new MyCovariantBox[B]())
useMyCovariantBox(new MyCovariantBox[C]())
// This class is contravariant (-T) with respect to its generic type paramter T, so you
// can use instances of MyContravariantBox[SuperclassOfT] wherever a MyContravariantBox[T] is required
class MyContravariantBox[-T]()
def useMyContravariantBox(contrabox: MyContravariantBox[B]): Unit = println("got a box of B (or super B)")
useMyContravariantBox(new MyContravariantBox[A]())
useMyContravariantBox(new MyContravariantBox[B]())
useMyContravariantBox(new MyContravariantBox[C]()) // ERROR: type mismatch: expected MyContravariantBox[B]
}
def theCoreConceptIsBasedOnFunctions(): Unit = {
class Organism() { def beOrganism(): Unit = println("I'm alive") }
class Animal() extends Organism { def beAnimal(): Unit = println("I'm an animal") }
class Dog() extends Animal { def woof(): Unit = println("I'm saying woof") }
class Cat() extends Animal { def meow(): Unit = println("I'm saying meow") }
val animals: ArrayDeque[Animal] = ArrayDeque(new Animal(), new Dog(), new Cat())
def makeAnimalAndAppend(animals: ArrayDeque[Animal], makeAnimal: (() => Animal)): Unit =
animals.append( makeAnimal() )
// So, what kind of functions match the type signature of makeAnimal?
makeAnimalAndAppend(animals, (() => new Organism())) // ERROR
makeAnimalAndAppend(animals, (() => new Animal()))
makeAnimalAndAppend(animals, (() => new Dog()))
// So, for functions of type F:(anything => T), functions of type F':(anything => SubclassOfT) are subtypes of F.
// This means that functions are covariant with respect to their return type
//
// The intuitive explanation is that if F returns T, the calling code is expecting a T, so it can
// receiving SubclassOfT instead, but not SuperclassOfT
def popAnimalAndUse(animals: ArrayDeque[Animal], useAnimal: (Animal => Unit)): Unit =
useAnimal( animals.removeLast() )
// So, what kind of functions match the type signature of useAnimal?
popAnimalAndUse(animals, ((o: Organism) => o.beOrganism()))
popAnimalAndUse(animals, ((a: Animal) => a.beAnimal()))
popAnimalAndUse(animals, ((d: Dog) => d.woof())) // ERROR
// So, for functions of type F:(T => anything), functions of type F':(SuperclassOfT => anything) are subtypes of F.
// This means that functions are contravariant with respect to argument type
//
// The intuitive explanation is that if F accepts args of type T, the calling code is expecting to be able to pass a T.
// If that code uses F' instead, it can still pass a T to F'
// The general idea that functions that 'give a T' can be safely replaced with functions that 'give a SubclassOfT',
// and functions that 'use a T' can be safely replaced with functions that 'use a SuperclassOfT'
// So, the Scala Programming Language defines the type for functions like this:
//
// Function[-A1, -A2, -A3, ..., -AN, +R]
//
// make sense?
}
def bringItAllTogether(): Unit = {
class Organism() { def beOrganism(): Unit = println("I'm alive") }
class Animal() extends Organism { def beAnimal(): Unit = println("I'm an animal") }
class Dog() extends Animal { def woof(): Unit = println("I'm saying woof") }
class Cat() extends Animal { def meow(): Unit = println("I'm saying meow") }
// 1.
// Why can't types that are covariant +T have methods with arguments of type T :
abstract class CovWrapper[+T]() {
def useT(t: T): Unit // the compiler complains here, because if it didn't, you could do this:
}
def useAnimalCovWrapper(wa: CovWrapper[Animal]): Unit = wa.useT(new Animal())
val wd: CovWrapper[Dog] = new CovWrapper[Dog]() { def useT(dog: Dog): Unit = dog.woof() }
// You should be able to use CovWrapper[Dog] instead of CovWrapper[Animal]
useAnimalCovWrapper(wd) // But you can't do wd.useT(new Animal()), because wd calls dog.woof() !
// 2.
// Why can't contravariant types -T have methods that return the type T :
abstract class ContraWrapper[-T]() {
def giveT(): T // compiler complains here, because if it didn't, you could do this:
}
def useAnimalContraWrapper(wa: ContraWrapper[Animal]): Unit = wa.giveT().beAnimal()
val wo: ContraWrapper[Organism] = new ContraWrapper[Organism]() { def giveT(): Organism = new Organism() }
// You should be able to use ContraWrapper[Organism] instead of ContraWrapper[Animal]
useAnimalContraWrapper(wo) // But you can't do wo.giveT().beAnimal(), because wo.giveT() returns Organism !
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment