Skip to content

Instantly share code, notes, and snippets.

@tjheslin1
Last active December 29, 2017 15:34
Show Gist options
  • Save tjheslin1/a1bcc52ce5c433867b3a7ae8990e6fde to your computer and use it in GitHub Desktop.
Save tjheslin1/a1bcc52ce5c433867b3a7ae8990e6fde to your computer and use it in GitHub Desktop.

Taken from Essential Scala by Noel Welsh and Dave Gurnell

Invariant: Foo[T]: Foo[A] and Foo[B] are unrelated regardless of the relationship between A and B

Covariant: Foo[+T] -> Foo[A] is a supertype ofFoo[B] if A is a supertype of B.

Contravariant: Foo[-T] -> Foo[A] is a subtype of Foo[B] if A is a supertype of B.

Example:

val shape: Shape = ???
val circle: Circle = ???

val shapeWriter: JsonWriter[Shape] = ???
val circleWriter: JsonWriter[Circle] = ???

def format[A](value: A, writer: JsonWriter[A]): Json = 
  writer.write(value)

We can combine circle with either writer because all Circles are Shapes. Conversely, we can’t combine shape with circleWriter because not all Shapes are Circles.

Using contravariance. JsonWriter[Shape] is a subtype of JsonWriter[Circle] because Circle is a subtype of Shape.

This means we can use shapeWriter anywhere we expect to see a JsonWriter[Circle].

Covariant Generic Sum Type Pattern

If A of type T is a B or C, and C is not generic, write:

sealed trait A[+T]
final case class B[T](t: T) extends A[T]
final case object C extends A[Nothing]

Contravariant Position Pattern

If A of a covariant type T and a method f of A complains that T is used in a contravariant position, introduce a type TT >: T in f:

case class A[+T] {
  def f[TT >: T](t: TT): A[TT]
}

Taken from Essential Scala by Noel Welsh and Dave Gurnell

What’s With the Names?

What’s the relationship between the terms “contravariance”, “invariance”, and “covariance” and these different kinds of functor?

If you recall from Sec on 1.6.1, variance affects subtyping, which is essentially our ability to use a value of one type in place of a value of another type without breaking the code.

Subtyping can be viewed as a conversion. If B is a subtype of A, we can always convert a B to an A.

Equivalently we could say that B is a subtype of A if there exists a function A => B. A standard covariant functor captures exactly this. If F is a covariant functor, wherever we have an F[A] and a conversion A => B we can always convert to an F[B].

A contravariant functor captures the opposite case. If F is a contravariant functor, whenever we have a F[A] and a conversion B => A we can convert to an F[B].

Finally, invariant functors capture the case where we can convert from F[A] to F[B] via a func on A => B and vice versa via a func on B => A.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment