CSnerd
CSnerd

Reputation: 2229

Contravariance vs Covariance in Scala

I just learned Scala. Now I am confused about Contravariance and Covariance.

From this page, I learned something below:

Covariance

Perhaps the most obvious feature of subtyping is the ability to replace a value of a wider type with a value of a narrower type in an expression. For example, suppose I have some types Real, Integer <: Real, and some unrelated type Boolean. I can define a function is_positive :: Real -> Boolean which operates on Real values, but I can also apply this function to values of type Integer (or any other subtype of Real). This replacement of wider (ancestor) types with narrower (descendant) types is called covariance. The concept of covariance allows us to write generic code and is invaluable when reasoning about inheritance in object-oriented programming languages and polymorphism in functional languages.

However, I also saw something from somewhere else:

scala> class Animal
    defined class Animal

scala> class Dog extends Animal
    defined class Dog

scala> class Beagle extends Dog
    defined class Beagle

scala> def foo(x: List[Dog]) = x
    foo: (x: List[Dog])List[Dog] // Given a List[Dog], just returns it
     

scala> val an: List[Animal] = foo(List(new Beagle))
    an: List[Animal] = List(Beagle@284a6c0)

Parameter x of foo is contravariant; it expects an argument of type List[Dog], but we give it a List[Beagle], and that's okay

[What I think is the second example should also prove Covariance. Because from the first example, I learned that "apply this function to values of type Integer (or any other subtype of Real)". So correspondingly, here we apply this function to values of type List[Beagle](or any other subtype of List[Dog]). But to my surprise, the second example proves Cotravariance]

I think two are talking the same thing, but one proves Covariance and the other Contravariance. I also saw this question from SO. However I am still confused. Did I miss something or one of the examples is wrong?

Upvotes: 15

Views: 3311

Answers (3)

Lakshmi Rajagopalan
Lakshmi Rajagopalan

Reputation: 61

Variance is used to indicate subtyping in terms of Containers(eg: List). In most of the languages, if a function requests object of Class Animal, passing any class that inherits Animal(eg: Dog) would be valid. However, in terms of Containers, these need not be valid. If your function wants Container[A], what are the possible values that can be passed to it? If B extends A and passing Container[B] is valid, then it is Covariant(eg: List[+T]). If, A extends B(the inverse case) and passing Container[B] for Container[A] is valid, then it is Contravariant. Else, it is invariant(which is the default). You could refer to an article where I have tried explaining variances in Scala https://blog.lakshmirajagopalan.com/posts/variance-in-scala/

Upvotes: 4

VonC
VonC

Reputation: 1323403

A Good recent article (August 2016) on that topic is "Cheat Codes for Contravariance and Covariance" by Matt Handler.

It starts from the general concept as presented in "Covariance and Contravariance of Hosts and Visitors" and diagram from Andre Tyukin and anoopelias's answer.

http://blog.originate.com/images/variance.png

And its concludes with:

Here is how to determine if your type ParametricType[T] can/cannot be covariant/contravariant:

  • A type can be covariant when it does not call methods on the type that it is generic over.
    If the type needs to call methods on generic objects that are passed into it, it cannot be covariant.

Archetypal examples:

Seq[+A], Option[+A], Future[+T]
  • A type can be contravariant when it does call methods on the type that it is generic over.
    If the type needs to return values of the type it is generic over, it cannot be contravariant.

Archetypal examples:

`Function1[-T1, +R]`, `CanBuildFrom[-From, -Elem, +To]`, `OutputChannel[-Msg]`

Regarding contravariance,

Functions are the best example of contravariance
(note that they’re only contravariant on their arguments, and they’re actually covariant on their result).
For example:

class Dachshund(
  name: String,
  likesFrisbees: Boolean,
  val weinerness: Double
) extends Dog(name, likesFrisbees)

def soundCuteness(animal: Animal): Double =
  -4.0/animal.sound.length

def weinerosity(dachshund: Dachshund): Double =
  dachshund.weinerness * 100.0

def isDogCuteEnough(dog: Dog, f: Dog => Double): Boolean =
  f(dog) >= 0.5

Should we be able to pass weinerosity as an argument to isDogCuteEnough? The answer is no, because the function isDogCuteEnough only guarantees that it can pass, at most specific, a Dog to the function f.
When the function f expects something more specific than what isDogCuteEnough can provide, it could attempt to call a method that some Dogs don’t have (like .weinerness on a Greyhound, which is insane).

Upvotes: 21

stew
stew

Reputation: 11366

That you can pass a List[Beagle] to a function expecting a List[Dog] is nothing to do with contravariance of functions, it is still because List is covariant and that List[Beagle] is a List[Dog].

Instead lets say you had a function:

def countDogsLegs(dogs: List[Dog], legCountFunction: Dog => Int): Int

This function counts all the legs in a list of dogs. It takes a function that accepts a dog and returns an int representing how many legs this dog has.

Furthermore lets say we have a function:

def countLegsOfAnyAnimal(a: Animal): Int

that can count the legs of any animal. We can pass our countLegsOfAnyAnimal function to our countDogsLegs function as the function argument, this is because if this thing can count the legs of any animal, it can count legs of dogs, because dogs are animals, this is because functions are contravariant.

If you look at the definition of Function1 (functions of one parameter), it is

trait Function1[-A, +B]

That is that they are contravariant on their input and covariant on their output. So Function1[Animal,Int] <: Function1[Dog,Int] since Dog <: Animal

Upvotes: 16

Related Questions