You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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, not 1.
from union types to their joins. For instance,
if ??? then Some(1) else None
gets inferred type Option[Int], not Some[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???thenValelseVar: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.
The text was updated successfully, but these errors were encountered:
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:
Quite often these traits "leak" into inferred types, which generally leads to frustration. For instance,
if we define
we get as inferred type
x: Set[Kind & Product & Serializable]
which is much less nice than justSet[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
gets inferred type
Int
, not1
.from union types to their joins. For instance,
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:
Hence, when we infer the type variable for
Set(if ??? then Val else Var)
we'd firstwiden the union to the join
Kind & Product & Serializable
. But then we drop the shape traitsProduct
andSerializable
from the intersection, so we getSet[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
andSerializable
, since these are silently added to case classes. Probably alsoComparable
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.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. inthe type of
y
would be simplyA
, theSerializable
is dropped. However,Serializable
can be retained ify
is typed explicitly:One question is whether we can restrict recursive inheritance to super traits. That would get very close to Tate's proposal. E.g.
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 writeinstead 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. Withsuper
traits, the library designer can make the decision once for all that a trait should not show up in inferred types. Withextends 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.
The text was updated successfully, but these errors were encountered: