I’d like to open the discussion for the pending SIP on dropping general type projection from the language. You can find a brief write up here.
Summary
Scala 2 currently allows general type projection of the form T#A where T is an arbitrary type and A names a type member of T.
Dotty prohibits this if T is an abstract type (class types, singleton types and type aliases are still permitted). This change was made because unrestricted type projection is unsound.
Implications
The use of type projection from abstract types has been used to implement type functions in Scala 2 (see eg. this encoding of the SKI combinator calculus). This will no longer be possible.
In mitigation, this way of encoding type functions was always limited and extremely fragile due to compiler bugs and confusion around the semantics, and it has largely been displaced by implicit-driven type level computation. In Dotty we also have match types which allow type functions to be expressed directly.
Opening this up for general community discussion and insight into any other use cases that should be considered.
Off the top of my head this seems like a good change. I can’t think of anything I use type projection for that type lambdas or match types won’t do better.
Dotty type lambdas replace Scala 2 type lambdas, which also use type projection, but in fact the type projections typically used in that case are still permitted under this proposal.
FWIW, my STM overlaying object system Lucre relies heavily on type projections, such as
trait Sys[S <: Sys[S]] {
type Id
type Tx
}
and then a proliferation of S#Tx in the entire code base. I don’t see a straight forward way to move this project (thus my entire project stack) to Dotty if S#Tx for S <: Sys[S] wasn’t supported. I experimented for a long time till I came up with this construction, everything else, including path depenendent types failed miserably.
The proposal is only to prohibit type projections when the type on the LHS is an abstract type. Could you give some examples of type projections from your codebase which also meet that condition?
Look at my second reply - it’s simply not possible to use path dependent types. I have really tried to make them work for a long period back when I designed the system. The crucial thing is to make the entire construction work, including the classes parametrised with S. Putting only path dep types in Txn is easy in isolation.
(And yes, it’s val system: S in all practical applications)
If you could rewrite to def gimme()(implicit tx: s.Tx): UseCase[s.type] it works again.
However what most of these examples of heavy type projection use cases eventually show IMO, is that once you start rewriting to hardcore path dependent types all-over you will eventually come into conflict with the type inference strategy of widening singleton types by default.
trait Sys
trait Obj {
erased val s: Sys
type S = s.type // error!
}
Is there a reason this doesn’t work in Dotty? If this worked, I could see an elegant way to porting my type projection based system to use path dependent types instead. For example: https://scastie.scala-lang.org/S3yD86zjSU6CX7y1i5A76g - it thus requires that the above is allowed, and that a non-erased val can override/implemented an erased val.
Unfortunately, yes. In fact, it’s the same reason why type projection is outlawed now. Both are unsound. To explain the issue very roughly: To maintain soundness we need to make sure that every prefix of a type selection has been constructed somewhere. That’s not true for type projections (since the prefix is a type) and it is also not true for erased types (since the prefix will never be constructed explicitly).
However, there might be a way to still allow this if we demand realizability instead. Question: Would your construction also work if s was not erased? I.e. is erased just an optimitzation to save space?