I have a difficulty with restrictions on type projections in Scala. Assume I have the following code:
sealed trait Color
case object Red extends Color
case object Green extends Color
case object Blue extends Color
trait Item {
type Colors <: Color
}
object RedGreenItem extends Item {
type Colors = Red.type with Green.type
}
object Test {
def foo[I <: Item, C >: I#Colors <: Color](item : I, color : C) : Unit = {
// ...
}
def main(args : Array[String]) : Unit = {
foo(RedGreenItem, Blue) // <- why does it compile?
}
}
What I actually want to achieve is to be able to pass tofoo
as the second parameter only a color that is defined in Item#Colors
(hence the restriction C >: I#Colors
).
But it turns out, I can pass every other color as well (Blue
in this case).
I can see the source for the problem if I write the foo
method like this:
def foo[I <: Item, C >: I#Colors <: Color : TypeTag](item : I, color : C) : Unit = {
// ...
println(s"type = ${typeOf[C]}")
}
// type = Color
So, instead of Blue.type
the type is inferred as Color
and therefore any color is okay.
If I remove the restriction >: I#Colors
, then the type is determined correctly:
def foo[I <: Item, C <: Color : TypeTag](item : I, color : C) : Unit = {
// ...
println(s"type = ${typeOf[C]}")
}
// type = Blue.type
So, my question is: How can I achieve my goal to have the restriction for the color to have an allowed type and not to compile otherwise? Many thanks in advance and sorry for maybe a stupid question.
Your type Colors
is basically saying that you want your color to be both Red
and Green
. But what you want to express is that it has to be Red
or Green
.
In Dotty, the future version of Scala, you can express that relatively directly as you wanted to:
sealed trait Color
sealed trait Red extends Color
sealed trait Green extends Color
sealed trait Blue extends Color
case object Red extends Red
case object Green extends Green
case object Blue extends Blue
trait Item {
type Colors <: Color
}
object RedGreenItem extends Item {
type Colors = Red | Green
}
def foo[I <: Item](item: I)(color: item.Colors): Unit = ()
The extra sealed traits are for circumventing an other restriction that might still be lifted. But this works:
scala> foo(RedGreenItem)(Red)
scala> foo(RedGreenItem)(Green)
scala> foo(RedGreenItem)(Blue)
-- [E007] Type Mismatch Error: <console>:13:18 ---------------------------------
13 |foo(RedGreenItem)(Blue)
| ^^^^
| found: Blue.type
| required: RedGreenItem.Colors
|
In current Scala I think the closest thing to model such a type member, in the general case, is with a Shapeless HList
.
import shapeless._, ops.hlist._
trait Item {
type Colors <: HList
}
object RedGreenItem extends Item {
type Colors = Red.type :: Green.type :: HNil
}
def foo[I <: Item, C <: Color](item: I, color: C)(implicit contains: Selector[item.Colors, C]): Unit = ()
And again this works as expected:
scala> foo(RedGreenItem, Red)
scala> foo(RedGreenItem, Green)
scala> foo(RedGreenItem, Blue)
<console>:27: error: Implicit not found: shapeless.Ops.Selector[RedGreenItem.Colors, Blue.type]. You requested an element of type Blue.type, but there is none in the HList RedGreenItem.Colors.
foo(RedGreenItem, Blue)
^
There are still other ways to express this. For instance with typeclasses instead of an overridable type member. I'm not recommending HLists
over those, but that's how you can do it if you want to go with type members.