Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop some traits from inferred type intersections #9028

Closed
odersky opened this issue May 22, 2020 · 1 comment
Closed

Drop some traits from inferred type intersections #9028

odersky opened this issue May 22, 2020 · 1 comment

Comments

@odersky
Copy link
Contributor

odersky commented May 22, 2020

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, 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 ??? 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.

@odersky
Copy link
Contributor Author

odersky commented May 22, 2020

Should this be part of 3.0? Maybe. The rationale would be that it could simplify type inference in many cases and therefore make porting easier.

odersky added a commit to dotty-staging/dotty that referenced this issue May 24, 2020
and eliminate super traits in widenInferred
odersky added a commit to dotty-staging/dotty that referenced this issue May 29, 2020
and eliminate super traits in widenInferred
anatoliykmetyuk added a commit that referenced this issue Jun 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant