Home > Mobile >  Scala: Map of functions on sum types
Scala: Map of functions on sum types

Time:06-03

Given the following code

sealed trait Fruit

case class Apple(color: String) extends Fruit
case class Orange(color: String) extends Fruit

def getAppleColor(apple: Apple) = apple.color
def getOrangeColor(orange: Orange) = orange.color

val myMap: Map[String, Fruit] = Map(
"myApple" -> Apple("red"),
"myOrange" -> Orange("orange"),
)

val myMapOfFunctions: Map[String, Apple with Orange => String]  = Map(
"myAppleColorFun" -> getAppleColor,
"myOrangeColorFun" -> getOrangeColor,
)

Why myMapOfFunctions is not a Map[String, Fruit => String] similarly to myMap? I guess because it is about functions but I'd like better to understand why. Thanks!

CodePudding user response:

I am just trying to understand why the compiler says that the type of the map is Apple with Orange and not Fruit

Okay, the good thing is that this is "easy" to explain.
The bad thing is that it may not be as easy to understand.

Let's take a couple of steps back, and let's simplify the code a little by using an if instead of a collection.

When you do something like this:

val foo = if (bar) x else y

The compiler has to infer the type of foo, for doing that it will first get / infer the types of x and y; let's call those X & Y respectively and then compute the LUB (least upper bound) between both, resulting in a new type Z which will be the type assigned to foo
This makes sense because X <: Z and Y <: Z and thus Liskov is respected.

Quick note, if X and Y are the same types A then the LUB is just A
Another quick one, if X is a subtype of Y then the LUB is simply Y

Let's see those applied to simple types:

val fruit = if (true) Apple(color = "red") else Orange(color = "green")

Here, one branch has the type Apple and the other Orange and the LUB between both is Fruit.
Everything has been straightforward until this point.

Now, let's spice the things up a little:

val optApple: Option[Apple] = Apple(color = "red")
val optOrange: Option[Orange] = Orange(color = "green")

val optFruit = if (true) optApple else optOrange

Here one branch is Option[Apple] and the other is Option[Orange], we know that the result will be Option[Fruit], but why?
Well, because Option was defined to be covariant on its type parameter thus Option[Fruit] is a supertype of both branches; and mainly the LUB.

Okay, but what happens with functions?

// Implementations do not matter.
val appleFunction : Apple => Apple = ???
val orangeFunction: Orange => Orange = ???

val fruitFunction = if (true) appleFunction else orangeFunction

In this case, the LUB will be (Apple with Orange) => Fruit ... but why?

Well, the return is easy since functions are also covariant on their returns which means the LUB will again be Fruit
But, why is the input not like that? Well, because functions are contravariant on their inputs, thus for a function f to be a subtype of another function g, the type of the input of f must be a super type of the input of g; i.e. it goes in the opposite order which is why it is called contravariance.

So that explains why the compiler inferred that type.
But, you may be wondering what this variance business is, why it matters, and why there is one that seems counterintuitive. But that is outside of the scope of this question & answer.
Nevertheless, I can share a couple of resources that may be useful:

  • Related