Here’s a proposal to add intersection types to Scala 3. An intersection type A & B replaces a compound type A with B. Intersection types have nice algebraic properties, in particular, they are commutative. Intersection types have been studied formally in DOT and are implemented in Dotty.
Intersection types have a solid theory and nice algebraic laws. They greatly improve in both respects on compound types.
Migration
For the moment, the compound syntax A with B is still available, in order to allow cross building between Scala 2 and 3. But it maps already to the intersection type semantics of A & B. Compound types will be deprecated in Scala 3.1. There is a already an implemented rewrite rule to move from with to &.
This proposal is open for discussion in the community and will be discussed in our next SIP meeting, where we’ll take all your feedback into account for the approval/dismissal of this feature.
I don’t understand something. with is not commutative and when we generate new classes/traits the order is important.
trait A {
val foo = 1
}
trait B extends A {
override val foo = 2
}
trait C extends A {
override val foo = 3
}
trait BC extends B with C
trait CB extends C with B
Why do we limit writing val bc : B with C and val cb : C with B ?
You mean, why not allow A with B in addition to A & B under the old non-commutative meaning? It would complicate the type system to no small degree. You’d have to think through all the interactions with other features. How does A with B relate to A & B? Can A with (B & A) be simplified? And so on…
Also, used as a type A with B does not seem to be that useful on its own. For what it’s worth in Dotty we always map it to A & B and have not found an incompatibility because of it.
Finally, we have the invariant that any class inheriting A with B must be a subtype of A & B. So the two types would be very closely related anyway.
Maybe that’s not the right place to clarify this but it may help understanding the proposal’s motivation. I don’t understand in what way compound types are not commutative. If I open a sbt console with scala 2.12.6, I can do the following:
scala> trait A
defined trait A
scala> trait B
defined trait B
scala> class C extends A with B
defined class C
scala> new C: A with B
res0: A with B = C@273fe26
scala> res0: B with A
res1: B with A = C@273fe26
If you provide type parameters it works, so I share @markarasev’s question.
scala> x[Int, String] == ()
res5: Boolean = true
scala> implicitly[(Int with String) =:= (String with Int)]
res6: Int with String =:= String with Int = generalized constraint
The problem is that the “user-level reflection” of the type system relation =:= is not really accurate. It actually tells you whether A <:< B and B <:< A, which is not the same thing as the type system’s idea of two types being the same. See this ticket, in which I document getting confused by this myself
scala> :power
Power mode enabled. :phase is at typer.
scala> trait A
defined trait A
scala> trait B
defined trait B
scala> typeOf[A with B] =:= typeOf[B with A]
res0: Boolean = false
scala> typeOf[A with B] <:< typeOf[B with A]
res1: Boolean = true
scala> typeOf[B with A] <:< typeOf[A with B]
res2: Boolean = true
AFAIK, a type is a set of possible values. Since A with B and B with A have the same possible values, they must be the same type. Or do you have a different definition of type?
There only is a difference when you are extending A with B. The way I see it, is that A with B is not just a type, but a type plus some extra information on how to extend it.
Because the with operator is not commutative in Scala 2, these types don’t have the same values. The order of the types in the intersection matters. Linearisation will come up with e.g., different result types for methods (last one wins).
Dotty fixes this by pushing down the intersection/union to union/intersection (for contravariant occurrences) or intersection/union (in covariant positions) in signatures of the type’s members. This is commutative.
A reference of type A with B can accept exactly the same values as a reference of type B with A. If you want to dispute that, then show me a value that one can accept but not the other one.
Here’s a contrived example of what I was thinking of:
scala> trait Base { def foo: Any }
defined trait Base
scala> trait A extends Base { override def foo: Int = 1 }
defined trait A
scala> trait B extends Base { override def foo: Any = "1" }
defined trait B
scala> new A with B
^
error: overriding method foo in trait A of type => Int;
method foo in trait B of type => Any has incompatible type
scala> new B with A
res1: B with A = $anon$1@7e62cfa3
scala> lazy val fooAB = (??? : A with B).foo
fooAB: Any = <lazy>
scala> lazy val fooBA = (??? : B with A).foo
fooBA: Int = <lazy>
These types don’t behave the same way in terms of generating values, nor in the typing of member selection. I believe these differences will go away in Scala 3.
@adriaanm Good example. Yes, these differences go away.
The tutorial page was a bit too simplistic here. Essentially, in DOT/Scala 3: A & B == B & A in the sense that they are mutually subtypes of each other. That implies that they “have the same values”.
In Scala 3, A with B and B with A are also mutually subtypes of each other. But as you note, applying an operation like member selection can give different results for A with B and B with A. This is excluded for intersection types.
I see that the difference between A with B and B with A matters not only after extends, but also after new. But it should still not matter for reference types.
That fooAB is inferred as Any, is a bug in the type inference. It must be Int.