2

A simple value hierarchy

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
}

Type safe selection of Values

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]

Selecting a value as Option[T] fails in Dotty

However, 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]

A bug in Dotty or what to do?

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?

stefanobaghino
  • 11,253
  • 4
  • 35
  • 63
mgd
  • 4,114
  • 3
  • 23
  • 32

2 Answers2

1

_.value has a dependent function type which isn't inferred by default but you can specify it:

def getValueInnerOption[V <: Value : ClassTag] = {
  getValues.headOption.map((_.value): (v: V) => v.T)
}

and then

val iOption: Option[Int] = values.getValueInnerOption[IntValue]
val sOption: Option[String] = values.getValueInnerOption[StringValue]

compiles.

But the problem is that I am not sure it (and getValueInner) should work. Because the inferred return types for them involve V#T (you can see them in an error message if you give wrong return type), and trying to specify them explicitly gives

V is not a legal path since it is not a concrete type

(see http://dotty.epfl.ch/docs/reference/dropped-features/type-projection.html)

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • Thank you for the reply. Yes, I also noticed in the REPL that the result type of getValueInner actually resolves to a type projection V#T which I thought did not exist in Dotty – and if you try to declare the return type explicitly as a type protection you get the error you wrote above. If your solution should not work (as you say it might not in future Dotty releases) is there an alternative way of accomplishing what I want? – mgd Nov 29 '19 at 13:10
  • BTW, isn't the cause of Dotty resolving the return type to a projection V#T that map operates on a collection (call it C) of values (in this case there is only one because C is Option) and therefore the return type of map cannot be C[v.T] since there could be more than one value (e.g. for a list) so the common supertype of v.T for a number of v's is V#T. Since T has an upper bound (for IntValue) of Int this is compatible with assigning it to a val declared as Option[Int]. – mgd Nov 29 '19 at 13:16
  • I don't think so (without some equivalent to your two-type-parameters approach), but I know relatively little about Dotty. – Alexey Romanov Nov 29 '19 at 13:17
  • 2
    @mgd The problem is that `V#T` is not supposed to be legal in the first place (in Dotty), so the supertype should be `Value#T` (which is legal) or even `Any`. And that's what apparently happens when inferring the type of `_.value` which gives you `Option[Any]`. – Alexey Romanov Nov 29 '19 at 13:19
1

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)
}
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • 1
    Elegant solution. The only caveat I can see is that InnerType needs to be updated whenever a new type is added to the Value hierarchy. So in practice trait Value will be sealed since subclasses can only be added if it is possible to modify the source file where InnerType resides. – mgd Nov 29 '19 at 14:39