Make type projections safe instead of dropping them #17904
Replies: 0 comments 45 replies
-
We'd need a worked out way to map this down to DOT. Otherwise there's no way to answer the question:
I believe it would be great if someone undertook this effort. Foundations have to come first, and they are not optional. |
Beta Was this translation helpful? Give feedback.
-
I agree on the principle. But AFAIK several current features of Dotty do not yet have DOT mappings either, such as higher-kinded types and match types. Both higher-kinded types and (restricted) type projections are important for backward compatibility with Scala 2. I think that should justify their inclusion even though they have not been formally proven correct yet. At least we have a good intuition that they are "morally" sound (but of course, that intuition may turn out to be wrong). |
Beta Was this translation helpful? Give feedback.
-
@LPTK Somebody must make a judgment call for unproven addition, based on what we know anyway. Unlike for higher kinds, we know type projections have caused big trouble before. I'm thinking again of how to add type projections to my proofs: one needs to define when values are in == Separately, re-reading #4583, it seems what you propose might be close to the original semantics for type projection in the 2003 paper:
|
Beta Was this translation helpful? Give feedback.
-
@Blaisorblade by "using the semantics I and Miles suggested", are you referring to this?
How could that be the intended semantics? It would mean that given any class A { self => class B { /* defs */ } } is to be understood as: class A$B(self: A) { /* defs */ }
class A { type B <: A$B } where |
Beta Was this translation helpful? Give feedback.
-
As I mentioned to @LPTK elsewhere. For the record, while Miles and I might not yet agree and he might be right, I favor using the union. Assigning this to myself, as I'll keep this among my TODOs in my theoretical work. |
Beta Was this translation helpful? Give feedback.
-
(Towards?) Soundness for type projections in CoqGood news! I've just formalized type projections in Coq (first version: Blaisorblade/dot-iris#250). And this seems to support most interesting typing rules, including lower bounds for projections out of realizable types. I've used a "set-theoretic" semantics suggested by @LPTK, on top of my work on guarded DOT ( The idea is to model Formally, This semantics should validate at least the following typing rules — variants of those rules appear in the above PR:
From those, we can derive the following rules for lower bounds:
This follows because
This follows from L <: p.A <: T#A. In practice, if you have an expression Dotty realizability checking might help showing such a This way, the restriction on (Proj-<:-L) deals elegantly with realizability issues: if type In particular, this ends up disregarding the lower bound of Why are projections covariant? Why does (Proj-<:-Proj) hold?Till now, I didn't believe this rule. Here's an informal proof using this intuitive set-theoretic semantics. If a value In sum, any My Coq proof formalizes the same intuition, but using a pretty different notion of "set" :-). Possible TODOs
Theoretical questions
Caveats
/cc @LPTK @sstucki @namin @TiarkRompf @odersky @smarter @robbertkrebbers. BTW, see https://dot-iris.github.io/coqdoc/D.Dot.semtyp_lemmas.tproj_lr.html for the hyperlinked source, for easier navigation. EDIT: Gitter reactions at https://gitter.im/lampepfl/dotty?at=5ecd2bcfb101510b201a600c. |
Beta Was this translation helpful? Give feedback.
-
Unassigned myself for the next steps — @smarter or @odersky want to assign somebody to figure out what should happen? |
Beta Was this translation helpful? Give feedback.
-
I don't think we have the resources to look at this until after 3.0 |
Beta Was this translation helpful? Give feedback.
-
Fair — it might be worth mentioning this somehow to people who'd need lots of effort to migrate from them, maybe in http://dotty.epfl.ch/docs/reference/dropped-features/type-projection.html. But I'll ask on scala/contributors gitter. |
Beta Was this translation helpful? Give feedback.
-
It seems that my framework's last version is whole dependent on type projection. Now remove type projection and the functional is becoming so limited. |
Beta Was this translation helpful? Give feedback.
-
@djx314 Then maybe wait to migrate to Dotty... |
Beta Was this translation helpful? Give feedback.
-
One of my main code is trait TypeParam {
type H
type T <: TypeParam
}
trait TypeContext {
type K[T <: TypeParam]
}
class EncoderTypeContext extends TypeContext {
override type K[T <: TypeParam] = Encoder[(T#H, T#T#H)]
} I use type projection to implement a HList type parameter context. Now I can just codegen TypeContext to TypeContext1 - TypeContext8, this makes my code increase to 10,000 lines and the support is limit. Use type project can also implement some code like a HList or a Nat plus Nat with no implict and no object(just type Nat3 = Nat1#Plus[Nat2]), it's worth to implenemting it in dotty. |
Beta Was this translation helpful? Give feedback.
-
@smarter could you elaborate on what kind of resources you'd need to look at this? From my point of view, most of the work is done. We just have to remove the current type projection limitation and instead prevent the use of their lower bounds. I could try to look into implementing it, if you think it makes sense. Waiting until after 3.0 sounds weird. It's like creating an artificial bump on the road (remove a feature, then add it again, even though the vast majority of uses of this feature are known to be sound). |
Beta Was this translation helpful? Give feedback.
-
@djx314 I can't comment on the details, Dotty will support HList and shapeless pretty well, thanks to work by Miles. |
Beta Was this translation helpful? Give feedback.
-
@Blaisorblade listed a bunch of TODOs in lampepfl/dotty-feature-requests#14 (comment), these look like major undertakings to me. |
Beta Was this translation helpful? Give feedback.
-
Yes — I mean, formally any member with bad bounds is a problem, and the case where |
Beta Was this translation helpful? Give feedback.
-
@Blaisorblade if you comment the 2 lines which use type projection in Also, interestingly, the reason You may be right that here is not the best place to discuss these things, though. |
Beta Was this translation helpful? Give feedback.
-
FWIW, I mechanized (a version of) that rule in Blaisorblade/dot-iris#304, after some changes to the model (in Blaisorblade/dot-iris#303). "Extra later connectives" are necessary — so what you get is essentially |
Beta Was this translation helpful? Give feedback.
-
I would just like to add my use case. My basic construction is this trait Ref[-Tx, A] {
def apply()(implicit tx: Tx): A
def update(v: A)(implicit tx: Tx): Unit
}
trait Txn[S <: Sys[S]] {
def newRef[A](init: A): Ref[S#Tx, A]
...
}
trait Sys[S <: Sys[S]] {
type Tx <: Txn[S]
...
} such that trait Foo[S <: Sys[S]] {
def bar()(implicit tx: S#Tx): Int
def baz()(implicit tx: S#Tx): Unit = {
val test = tx.newRef(bar())
}
} There are more type members involved, this is just a simplification (the API is easier to make compile than to create an actual implementation!). |
Beta Was this translation helpful? Give feedback.
-
Where do things stand now, are type projections permanently dropped, or will they perhaps be reintroduced post-3.0? Really unfortunate for those of us that are trying to port Scala 2 projects to Scala 3 where workarounds for "missing" type projections are deeply disatisfying. Such a useful feature, out with the bath water goes the baby... |
Beta Was this translation helpful? Give feedback.
-
Perhaps it would be an idea to get a list of projects that are going to go through a lot of work without type projections. It would be interesting there to see which of them are better off reworking their code, and which are going to be uglier (assuming a fix like the one proposed here). This would allow us to gather some experience of how to make the move, or indeed to show the value of the feature proposed here. I just got banana-rdf as close as possible to Scala3. But I now have 510 errors left due to type projections. It would be good to know if those type projections are ok with this proposal, or if they are in fact the dangerous ones the removal was attempting to protect us from. |
Beta Was this translation helpful? Give feedback.
-
@bblfish are any of the types you project out explicitly lower-bounded? And if so, is the lower bound ever used? (Meaning something is taken as a subtype of the type projection, e.g. a value is upcasted into it.) If not, then you should be good, assuming the powers that be decide to reintroduce type projection in a later release of Scala 3. |
Beta Was this translation helpful? Give feedback.
-
I think we should be ok. The trait type projection is used with is RDF and its various subtypes Jena, RDF4J and Plantain all are immediately used by a single object (each named at the bottom of those files respectively) so I think that counts as lower bounded. The advantage is that it allows one to write code that is completely independent of the RDF implementations as shown by all of our tests such as this one. It allows one to write libraries in Java and JS (we have not tried native yet), by abstracting the implementations. I don't know how @betehess knew this was the right thing to do when he implemented it 10 years ago. |
Beta Was this translation helpful? Give feedback.
-
@bblfish your usage should be completely fine. On the other hand, it seems it would be rather easy to port to using a value |
Beta Was this translation helpful? Give feedback.
-
You mean using dependent types? I started looking at that in August. trait PointedGraph[T <: RDFObj](using val rdf: T) {
def pointer: rdf.Node
def graph: rdf.Graph
} But with 500 errors to fix I wanted to make sure I was going in the right direction. So I thought I needed a lot more experience with Scala3, and I still think I could do with more. It somehow seemed to lead to creating a bunch of empty brackets and made me wonder if a notation like class PG[val t: RDF](node: t.Node, gr: t.Graph) could be helpful. Otherwise it seems like bracketing issues may be re-appearing. If I remember the ordering of dependent types is tricky as arguments often depend on the dependent object... |
Beta Was this translation helpful? Give feedback.
-
The challenge is, if you are using type projections without lower bounds, despite it being safe, you either have to remove them before adopting scala 3 or wait an extra cycle. I guess most people will remove them and do the port already, so adding them back later will wind up only costing more effort or encouraging people not to port the given library. Summingbird uses abstract types, ( |
Beta Was this translation helpful? Give feedback.
-
@johnynek note that no one forces you to port it back to using type projection in 3.1. That would be a weird thing to do. My guess is that most people will just abandon type projections altogether or just wait before porting their libraries to Scala 3. This is really a suboptimal situation, as the former option creates needless work and breaking changes (and can also lead to less user-friendly library interfaces), and the latter option means the community is stuck on an old version of Scala for an unknown amount of time. And all this pain is inflicted on users for pretty much no reason. There are many existing actual soundness holes in Scala, and it's not the end of the world. I have never heard of anyone being affected by the particular one that comes from Scala 2-style type projection. So even keeping unsound type projections would be a better way forward, before the restriction that makes them sound can be put in. |
Beta Was this translation helpful? Give feedback.
-
Clearly in Scala 3 there's been a lot of work put into proving that the language is theoretically sound; removing type projections puts theory into practice, but at the expense of rendering existing real world applications that depend on this feature, useless. I personally have relied heavily on type projections over the years, and as a result will simply abandon all affected Scala 2 code -- a fresh start of sorts, far from ideal, but the alternative of reworking type projections into path dependent type + givens simply doesn't pay its weight. |
Beta Was this translation helpful? Give feedback.
-
@LPTK I guess I don't understand why the suggestion of: lampepfl/dotty-feature-requests#14 (comment) was not done. It seems like a one line diff and probably restores 90% of the use cases without adding soundness holes. Do I misunderstand? |
Beta Was this translation helpful? Give feedback.
-
Background
It seems that type projections, which are of the form
T # X
, could be made sound if we simply disregarded the lower bound ofX
inT
unlessT
is a singleton type.For example:
See also the discussions on the contributors forum.
(Note: we could just as well disregard the upper bound instead, but I suppose that would be much less useful.)
Pratical Motivation
Type projections are used quite a lot in Scala 2. It would be best if we could just port most of that code as is, since migrating to not using type projections requires extensive refactoring, cannot be automated, and may introduce runtime performance concerns (see the same forum discussion).
The thing is, it seems it would be possible to port most of that existing Scala 2 code to a system with the restriction I proposed above.
I have seen lots of code where (non-trivial) upper bounds of type projections were used. I don't remember seeing lots of code where lower bounds were used. And I'm pretty sure I've never seen code where both bounds of a type projections were used, except in constructions meant to show the unsoundness of type projections.
Philosophical Motivation
Match types are a great addition to the language, in that they give us a concise and lightweight way of expressing closed type families. With #5996 they should even become robust in the face of abstract types.
Dually, type projections have been great because they basically give us concise and lightweight syntax for expressing open type families. If we add closed type families to Scala, it makes sense to me that we should strive to continue supporting open ones!
Semantics
What does a type projection
T # X
mean? I would say it means some type with a known upper bound, on which we know nothing more... that is, untilT
is substituted with a type in whichX
is concrete (at which point we can just pick that type out), or substituted with a singleton type (at which point we have a classical path-dependent type). It's easy to see that in both situations, such substitution will satisfy the upper bound that we have assumed forT # X
.Is that kind of weird? Well, IMHO it's no weirder than the nature of a non-reduced match type: a type on which we just know an upper bound (the union of the cases), but that is only made to precisely mean something when the scrutinee becomes concrete enough that the match stops being ambiguous.
Have I missed something? Would that actually be unsound?
Beta Was this translation helpful? Give feedback.
All reactions