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
andY
are the same typesA
then the LUB is justA
Another quick one, ifX
is a subtype ofY
then the LUB is simplyY
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: