Description
Ross Tate et al make an interesting observation in their paper "Getting F-bounded polymorphism into shape": Traits split often cleanly into "materials" and "shapes". Materials in their terminology are traits that are (parts of) types of vals and defs whereas shapes are only used as super-traits of other classes and traits. They point out that traits inherited recursively that give rise to F-bounded polymorphism are in practice always shapes, and suggest that algorithms for subtyping and type checking could be simplified if this rule was enforced.
Some traits in the Scala universe that are good candidates for shapes:
Product
Serializable
Comparable
IterableLike
IndexedSeqOptimized
Quite often these traits "leak" into inferred types, which generally leads to frustration. For instance,
if we define
trait Kind
case object Var extends Kind
case object Val extends Kind
val x = Set(if ??? then Val else Var)
we get as inferred type x: Set[Kind & Product & Serializable]
which is much less nice than just Set[Kind]
.
In Scala 3, we have a systematic way to widen types before they become inferred types for vals, defs, or type variables. Right now we apply the following widenings
-
from singleton types (including constant types) to their underlying type. For instance
val x = 1
gets inferred type
Int
, not1
. -
from union types to their joins. For instance,
if ??? then Some(1) else None
gets inferred type
Option[Int]
, notSome[Int] | None
.
Either widening is disabled if it conflicts with a bound or expected type given for the inferred type.
I think it would be interesting to add a widening that dropped "shape traits" from inferred types. So in the example above we'd have:
if ??? then Val else Var: Val | Var
Hence, when we infer the type variable for Set(if ??? then Val else Var)
we'd first
widen the union to the join Kind & Product & Serializable
. But then we drop the shape traits
Product
and Serializable
from the intersection, so we get Set[Kind]
as end result.
What is a Shape Trait?
One answer would be to just have a set of fixed traits that are known to have caused troubles in the past. Definitiely Product
and Serializable
, since these are silently added to case classes. Probably also Comparable
since that is usually inherited recursively, and often causes lubs to blow up.
A more scalable alternative is to give library designers control whether a trait is a shape trait or not. I propose to use the existing keyword super
for that. I.e.
package scala
super trait Serializable extends java.lang.Serializable
would establish Serializable
as a trait that is designed specifically to be a super trait of other traits and not a type in its own right. Super traits would be dropped from inferred types. E.g. in
val x: A & Serializable = ...
val y = x
the type of y
would be simply A
, the Serializable
is dropped. However, Serializable
can be retained if y
is typed explicitly:
val y: A & Serializable = x
One question is whether we can restrict recursive inheritance to super traits. That would get very close to Tate's proposal. E.g.
class X extends T[X]
would be legal only if T
was declared a super trait. I think that would be attractive in the long run, since it would probably steer people away from patterns that make type inference behave in bad ways. But we can do that only over time.
An Alternative?
An alternative solution would be to classify the way a trait is inherited rather than the trait itself. So, keeping with super
, we'd write
class A extends super Product with super Serializable
class C extends super T[X]
instead of marking the traits themselves with super
. This is more flexible, but it turns out to be a lot more complex to specify and implement. Also, it is easier to mis-use. With super
traits, the library designer can make the decision once for all that a trait should not show up in inferred types. With extends super
, every implementer of an extending class has to make that decision again.
Since Tate et al's paper indicates that it's usually easy to tell whether a trait is a shape or material, it seems better to go with the simpler scheme.