Imagine this simple trait Value
where every implementing class has a value
of some type T
.
trait Value {
type T
def value: T
}
We have two different implementing classes representing Int
and String
values respectively.
case class IntValue(override val value: Int) extends Value {
override type T = Int
}
case class StringValue(override val value: String) extends Value {
override type T = String
}
If we have a List
of values we would like to have a type safe way of selecting all values of a specific type. Class Values
and its companion object help us doing that:
object Values {
private type GroupedValues = Map[ClassTag[_ <: Value], List[Value]]
def apply(values: List[Value]): Values = {
val groupedValues: GroupedValues = values.groupBy(value => ClassTag(value.getClass))
new Values(groupedValues)
}
}
class Values private (groupedValues: Values.GroupedValues) {
// Get a List of all values of type V.
def getValues[V <: Value : ClassTag] = {
val classTag = implicitly[ClassTag[V]]
groupedValues.get(classTag).map(_.asInstanceOf[List[V]]).getOrElse(Nil)
}
def getValue[V <: Value : ClassTag] = {
getValues.head
}
def getValueOption[V <: Value : ClassTag] = {
getValues.headOption
}
def getValueInner[V <: Value : ClassTag] = {
getValues.head.value
}
}
All this works fine in both Scala 2.13 and Dotty 0.20.0-RC1 so having a list of mixed values…
val valueList = List(IntValue(1), StringValue("hello"))
val values = Values(valueList)
…we can select elements and get them returned as the correct type – all checked at compile-time:
val ints: List[IntValue] = values.getValues[IntValue]
val strings: List[StringValue] = values.getValues[StringValue]
val int: IntValue = values.getValue[IntValue]
val string: StringValue = values.getValue[StringValue]
val intOption: Option[IntValue] = values.getValueOption[IntValue]
val stringOption: Option[StringValue] = values.getValueOption[StringValue]
val i: Int = values.getValueInner[IntValue]
val s: String = values.getValueInner[StringValue]
Option[T]
fails in DottyHowever, if we add this function to select values as their T
type (i.e. Int
and String
) and get it returned as an Option
…
class Values ... {
...
def getValueInnerOption[V <: Value : ClassTag] = {
getValues.headOption.map(_.value)
}
}
…then things work fine in Scala 2.13:
val iOption: Option[Int] = values.getValueInnerOption[IntValue]
val sOption: Option[String] = values.getValueInnerOption[StringValue]
But in Dotty 0.20.0-RC1 this does not compile:
-- [E007] Type Mismatch Error: getValue.scala:74:29
74 | val iOption: Option[Int] = values.getValueInnerOption[IntValue]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| Found: Option[Any]
| Required: Option[Int]
-- [E007] Type Mismatch Error: getValue.scala:75:32
75 | val sOption: Option[String] = values.getValueInnerOption[StringValue]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| Found: Option[Any]
| Required: Option[String]
We can fix the problem by adding a type parameter to getValueInnerOption
that ties the return type and the abstract type T
together and allows us to specify the return type.
def getValueInnerOption[V <: Value {type T = U} : ClassTag, U]: Option[U] = {
getValues.headOption.map(_.value)
}
Unfortunately, this means that we will have to add the actual type of T
(i.e Int
or String
) at the call site which is a pity because it is just boilerplate.
val iOption: Option[Int] = values.getValueInnerOption[IntValue, Int]
val sOption: Option[String] = values.getValueInnerOption[StringValue, String]
It seems that Dotty already knows what the upper bound of T
is but cannot propagate that knowledge to the result type of the function. This can be seen if trying to ask for a String
from an IntValue
:
-- [E057] Type Mismatch Error: getValue.scala:75:39
75 | val wtf = values.getValueInnerOption[IntValue, String]
| ^
|Type argument IntValue does not conform to upper bound Value{T = String}
so is the original code (without type parameter U
) something that can be expected to work in the final Scala 3.0 or does it need to be written in a different way?
In Dotty try match types as a replacement for type projections
type InnerType[V <: Value] = V match {
case IntValue => Int
case StringValue => String
}
trait Value {
type This >: this.type <: Value
type T = InnerType[This]
def value: T
}
case class IntValue(override val value: Int) extends Value {
override type This = IntValue
}
case class StringValue(override val value: String) extends Value {
override type This = StringValue
}
def getValueInner[V <: Value { type This = V } : ClassTag]: InnerType[V] = {
getValues.head.value
}
def getValueInnerOption[V <: Value { type This = V } : ClassTag]: Option[InnerType[V]] = {
getValues.headOption.map(_.value)
}