Variance of Scala List map function

308 Views Asked by At

I have a question that's been bugging me. Lists in Scala are covariant (List[+A])

Let's say we have these classes:

class A  
class B extends A

The map function of List[B] takes a function f: B => C

But I can also use a f: A => C which is a subclass of f: B => C
and it totally makes sense.

What I am currently confused by is that the map function should accept only functions that are superclasses of the original function (since functions are contravariant on their arguments), which does not apply in the example I've given.

I know there's something wrong with my logic and I would like to enlightened.

2

There are 2 best solutions below

1
On

As you already suspected, you are mixing up things here.

On the one hand, you have a List[+A], which tells us something about the relationships between List[A] and List[B], given a relationship between A and B. The fact that List is covariant in A just means that List[B] <: List[A] when B <: A, as you know already know.

On the other hand, List exposes a method map to change its "contents". This method does not really care about List[A], but only about As, so the variance of List is irrelevant here. The bit that is confusing you here is that there is indeed some sub-typing to take into consideration: map accepts an argument (a A => C in this case, but it's not really relevant) and, as usual with methods and functions, you can always substitute its argument with anything that is a subtype of it. In your specific case, any AcceptedType will be ok, as long as AcceptedType <: Function1[A,C]. The variance that matters here is Function's, not List's.

0
On

Your error lies in the assumption that map(f: A => C) should only accept functions that are superclasses of A => C.

While in reality, map will accept any function that is a subclass of A => C.

In Scala, a function parameter can always be a subclass of the required type.

The covariance of A in List[A] only tells you that, wherever a List[A] is required, you can provide a List[B], as long as B <: A.

Or, in simpler words: List[B] can be treated as if it was a subclass of List[A].

I have compiled a small example to explain these two behaviours:

class A  
class B extends A

// this means: B <: A

val listA: List[A] = List()
val listB: List[B] = List()

// Example 1: List[B] <: List[A]
// Note: Here the List[+T] is our parameter! (Covariance!)

def printListA(list: List[A]): Unit = println(list)

printListA(listA)
printListA(listB)

// Example 2: Function[A, _] <: Function[B, _]
// Note: Here a Function1[-T, +R] is our parameter (Contravariance!)

class C

def fooA(a: A): C = ???
def fooB(b: B): C = ???

listB.map(fooB)
listB.map(fooA)

Try it out!

I hope this helps.