-
Notifications
You must be signed in to change notification settings - Fork 2
Make type projections safe instead of dropping them #14
Comments
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. |
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). |
@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 scala/scala3#4583, it seems what you propose might be close to the original semantics for type projection in the 2003 paper:
|
@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 |
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. |
(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. |
I don't think we have the resources to look at this until after 3.0 |
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. |
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. |
@djx314 Then maybe wait to migrate to Dotty... |
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. |
@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). |
@djx314 I can't comment on the details, Dotty will support HList and shapeless pretty well, thanks to work by Miles. |
@Blaisorblade listed a bunch of TODOs in #14 (comment), these look like major undertakings to me. |
@smarter these TODOs (note: they have been edited) look like smaller details to me and can certainly be worked out later or as we go. I don't think you can reasonably hold the position that they're real blockers for a feature, while on the other hand full support is added to the compiler for such wild things as |
I mean, I think that at a minimum it'd be good to figure out if what TypeComparer does actually matches the rules in Blaisorblade/dot-iris#250, but that's just my opinion. (I'm also not a big fan of |
It's a generic lib like shapeless in first step with unlimited type parameter and limited item count intermediate data structure(Tuple)
https://github.com/djx314/ugeneric-compare
Shows that it has 3 times faster than circe semiauto in scala 2.13, the speed will more and more faster than semiauto when the property count becoming so large.
I don's konw what will happen in dotty because macros module now not yet compat to dotty. But shapeless3 is really fast in dotty.
But without type projection, the lines of code is 10 times larger than last version. That's why I pay attention to this issue.
…------------------ 原始邮件 ------------------
发件人: "Paolo G. Giarrusso"<[email protected]>;
发送时间: 2020年5月28日(星期四) 晚上10:24
收件人: "lampepfl/dotty-feature-requests"<[email protected]>;
抄送: "水山清风"<[email protected]>;"Mention"<[email protected]>;
主题: Re: [lampepfl/dotty-feature-requests] Make type projections safe instead of dropping them (#14)
@djx314 I can't comment on the details, Dotty will support HList and shapeless pretty well, thanks to work by Miles.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
@smarter The "other issues" by @milessabin were not about projections. I agree with @LPTK the other concerns don't seem showstoppers. While I'm in favor of being careful, I'd recommend:
|
@Blaisorblade Should it support by a single compile parameter? It seem's that scala2 cannot use given and so on. It's not friendly coding with scala3. |
We'd still need to figure out what exactly we can support , @LPTK's original proposal is "disregard the lower bound of X in T unless T is a singleton type.", so there's some work to be done in TypeComparer any way to see what needs to be tweaked. |
TypeComparer doesn’t care, there’s an error message in PostTyper, that could be made a warning in compatibility mode; that’s clearly a hack, but I think this already happens (see -strict vs not). Slightly harder: only use lower bounds when Typer itself can be certain they are safe. That might reject some currently allowed and safe programs. And you’re right, doing this properly is hard: actually annoying to implement: IIRC, right now Typer ignores the problem (because it basically does type inference) and PostTyper later rejects type projections out of non-realizable types - because realizability can only be checked after Typer (it needs global information; maybe that can be done in Typer, but I’d worry of CyclicReferenceErrors). BTW, I was never confident that worked (what about type projections that the user didn’t write? How do we know they don’t arise?), but have never seen an actual bug with it. But now, you’d have to record if you used the lower bound rule and if that actually mattered — as opposed to, instead, you backtracked. And what if you used lower bounds, they were unsafe, but the overall subtyping held for another reason (because there were unions involved and you could have backtracked to a “safe” subtyping)? Argh. |
Can't we add as a precondition to that rule in TypeComparer that the lower and upper bound are equal? |
FWIW, I do conjecture However, that doesn't carry over to all subtypes of |
Isn't this derivable? Isn't |
I guess in this particular case it holds, but it gets complicated when |
@Blaisorblade re: |
I see. If |
@LPTK that still worries me — lots of things in
But I'm not sure we should discuss that quick change here. |
Yes — I mean, formally any member with bad bounds is a problem, and the case where |
@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. |
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 |
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!). |
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... |
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. |
@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. |
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. |
@bblfish your usage should be completely fine. On the other hand, it seems it would be rather easy to port to using a value |
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... |
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, ( |
@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. |
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. |
@LPTK I guess I don't understand why the suggestion of: #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? |
We already implemented in essence the rule suggested in lampepfl/dotty-feature-requests#14: ``` Γ ⊨ p : T ------------------------ (Sel-<:-Proj) Γ ⊨ p.A <: T#A ``` This rule is implemented in `isSubPrefix`. But we did not get to check that since we concluded prematurely that an alias type would never match. The alias type in question in i17064.scala was ```scala Outer#Inner ``` Since `Outer` is not a path, the asSeenFrom to recover the info of `Outer#Inner this got approximated with a `Nothing` argument and therefore the alias failed. It is important in this case that we could still succeed with a `isSubPrefix` test, which comes later. So we should not return `false` when the prefix is not a singleton. Fixes scala#17064
We already implemented in essence the rule suggested in lampepfl/dotty-feature-requests#14: ``` Γ ⊨ p : T ------------------------ (Sel-<:-Proj) Γ ⊨ p.A <: T#A ``` This rule is implemented in `isSubPrefix`. But we did not get to check that since we concluded prematurely that an alias type would never match. The alias type in question in i17064.scala was ```scala Outer#Inner ``` Since `Outer` is not a path, the asSeenFrom to recover the info of `Outer#Inner` got approximated with a `Nothing` argument and therefore the alias failed. It is important in this case that we could still succeed with a `isSubPrefix` test, which comes later. So we should not return `false` when the prefix is not a singleton. Fixes #17064
We already implemented in essence the rule suggested in lampepfl/dotty-feature-requests#14: ``` Γ ⊨ p : T ------------------------ (Sel-<:-Proj) Γ ⊨ p.A <: T#A ``` This rule is implemented in `isSubPrefix`. But we did not get to check that since we concluded prematurely that an alias type would never match. The alias type in question in i17064.scala was ```scala Outer#Inner ``` Since `Outer` is not a path, the asSeenFrom to recover the info of `Outer#Inner this got approximated with a `Nothing` argument and therefore the alias failed. It is important in this case that we could still succeed with a `isSubPrefix` test, which comes later. So we should not return `false` when the prefix is not a singleton. Fixes scala#17064
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 scala/scala3#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?
The text was updated successfully, but these errors were encountered: