-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
RFC: Partial Types #3420
RFC: Partial Types #3420
Conversation
text/0000-partial_types.md
Outdated
Safe, Flexible controllable partial parameters for functions and partial not consumption (including partial not borrowing) are highly needed and this feature unlock huge amount of possibilities. | ||
|
||
Partial borrowing is already possible in Rust, as partial referencing and partial moves. | ||
|
||
But partial parameters are forbidden now, as qualified consumption: partial not borrowing, partial not referencing, partial not moving and partial initializing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like the motivation for partial borrows is very clear, but the motivation for this implementation of it is not. "Unlocking huge amounts of possibilities" doesn't seem terribly convincing to me; for example, we could implement the duck-typing templates system from C++ to unlock loads of possibilities, but we don't because it's clunky, errors at monomorphisation time by design, and encourages bad code. What real cases does this feature unlock that make it feel worthwhile?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First, this proposal almost remain style of using Rust as already is (by contrast to duck-typing).
- It is full backward-compatible. This proposal DO NOT change or block any existed feature! Before and after implementation.
- It adds some safe flexibility to safe code by safe methods.
- Simplicity in binary - Type integrity just say by type to compiler, that some fields are forbidden to use for everyone ever. And that allows to use ordinary references as "partial" and ordinal variables as "partial". No extra actions with variables or pointers are needed.
- Any partial type error is a compiler error, all types are erased after type-check, so no extra-cost in binary is needed.
- it is universal rule - that mean minimal special cases on implementation.
- It is minimal universal-extension - all other proposals propose less than this with more or same cost
text/0000-partial_types.md
Outdated
|
||
We need detailed integrity to write non-virtual specific typed parameters in function, including trait implementation. | ||
|
||
An abstractions is added for integrity detailing, we assume that **every** variable is a `struct`-like objects (even if it is not). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What value do we gain from assuming that every variable is a struct? It doesn't really feel simpler to me, and all it requires is adding extra .self
specifications to the end of every path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question.
- We get unification
- Enum allow unit-like values anyway (regardless if we want to represent numbers or not)
text/0000-partial_types.md
Outdated
p1 : &mut %{self.*} Point; | ||
p1 : &mut %{self.x, self.y, self.z, self.t, self.w} Point; | ||
p1 : &mut %{self.{x, y, z, w}} Point; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would make way more sense to me if these were listed as Self::field
instead of self.field
, especially with *
, for two reasons:
- One, it looks closer to the existing
use
syntax - Two,
Self::
is used for type information whereasself.
is used for expressions, and this is type information
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. I've change to "self.
" to "Self::
".
But it has a disadvantage on Impl, when we get "Inner Self " and "outer Self".
// case (D7)
impl Point {
pub fn x_refmut(&mut self : &mut %{Self::x, %any} Self) -> &mut f64 {
&mut self.x
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I see, in detailed access can be nothing else then Self::
, so it is fully unnecessary and I remove it.
So, code looks prettier!
impl Point {
pub fn x_refmut(&mut self : &mut %{x, %any} Self) -> &mut f64 {
&mut self.x
}
}
text/0000-partial_types.md
Outdated
We assume, that each field could be in one of 2 specific field-integrity - `%fit` and `%deny`. | ||
|
||
We also must reserve as a keyword a `%miss` field-integrity for future ReExtendeded Partial Types, which allows to create **safe** self-referential types. | ||
|
||
`%fit` is default field-integrity and it means we have an access to this field and could use it as we wish. But if we try to access `%deny` field it cause a compiler error. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to be honest, even after reading this multiple times, this doesn't make any sense to me at all. It doesn't help that "fit" is a word that has other meanings, and doesn't immediately strike me as being an abbreviation of "field integrity."
If the purpose of partial types is to only borrow a portion of a type, then why would you need to label what's included? Shouldn't the type be fully defined by what portion is being used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I've replaced "fit" to "permit" (and "integrity" to "access") for more obvious meaning.
About labels : it is a good question.
What is really change? It just add additional labeling wanted to borrow fields.
text/0000-partial_types.md
Outdated
or use `where` clause if integrity is extra verbose: | ||
```rust | ||
// case (D6) | ||
// FROM case (D5) | ||
|
||
fn x_store(&mut p1 : &mut %fit_sv_x PointExtra, & p2 : & %fit_x PointExtra) | ||
where %fit_sv_x : %{self.saved_x, %any}, | ||
%fit_x : %{self.x, %any} | ||
{ | ||
*p1.saved_x = *p2.x | ||
} | ||
|
||
fn x_restore(&mut p1 : &mut %fit_x PointExtra, & p2 : & %fit_sv_x PointExtra) | ||
where %fit_sv_x : %{self.saved_x, %any}, | ||
%fit_x : %{self.x, %any} | ||
{ | ||
*p1.x = *p2.saved_x; | ||
} | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem like good syntax to me, since where is usually used to indicate bounds on parameters, whereas this is defining parameters instead. Wouldn't it make more sense to simply use type aliases for this?
For example, type PointSaveXMut<'a> = &'a mut %{self.saved_x, %any} PointExtra
could then allow using PointSaveXMut<'_>
directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point! I like it! I added your example
So, I've left a lot of comments, but I have two main thoughts on the RFC. The first one is that this RFC is… very difficult to understand. I don't think it properly explains things, it makes a few weird assumptions about the type system, and doesn't explain the motivation properly. However, after thinking about it, I do think that partial types in some form is probably the best solution to the partial borrow problem. It makes a lot of sense to simply ignore parts of a type itself when borrowing, since the language already considers it unsafe to write to padding and otherwise "undefined" portions of structs. The flexibility is also justified because it helps avoid the case where you need to create dedicated "subtypes" when refactoring something that operates on a larger struct so that you can split up the data properly. This RFC recommends partial types through the guise of "integrity," which doesn't really make sense. The defining feature of partial types should just be what primitive fields are included, and there shouldn't be any extra caveats on that. If a particular field needs special treatment, we have dedicated wrapper types for that, like Additionally, this RFC doesn't comment at all on how arrays, enums, and unions are included. It makes sense to not include anything besides basic structs and integer primitives at first, but not knowing at all how this could be extended to, for example, enums, makes it difficult to adopt. Partial types on enums in particular explicitly requires enum subset types, where only particular variants are allowed; otherwise, how can you choose what fields to borrow if you're not sure if they even exist? Essentially… I wouldn't be in favour of this RFC being adopted, even its syntax, without heavy revision. However, I like the idea of partial types, and feel like I would like to see them in the language. |
I would say that it's actually the opposite: writing to |
Also what is a "sub-full_type", which is mentioned at the start of the guide section?
This is probably the most unlikely part of the proposal. I cannot imagine that being a realistic outcome. |
@clarfonthey Thanks for review!
A good advantage to my proposal is allowing incremental adding detailed access to different types, assuming that rest are "%full" but has no internal structure (like Units/ numbers). Struct and Tuples are first and most important candidates to have detailed access. Unions from the type system (and detailed access) are indistinguishable from Structs, but since Units were unsafe already, they remain to be unsafe with detailed access. For arrays and vectors it is unclear how to add detailed access. |
@Lokathor |
Huh, I wasn't aware this is how the semantics actually work. Is there any actual benefit to "poisoning" the padding bytes like this, or could this potentially be modified for the case of partial types? |
It means that you can put the type in a register, or two registers, or in memory, and not have to worry about preserving the padding bytes (depending on where the data is stored it may be faster to either copy the padding or clear it, or even put it in a place where there is no data storage for the padding at all).
I don't see any reason why not; the compiler has all the information regarding a partial type copy and can desugar it to a series of field copies. I imagine there are some restrictions on when a partial type copy is legal though, since the borrow checker supports paths like |
Isn't https://github.com/nikomatsakis/fields-in-traits-rfc the proper venue for partial borrowing? I do wonder if "fields" maybe a distraction here, like maybe trait should've associated lifetime-like objects which limit method disjointness, without saying how the impls realize the disjointness.
I've not thought this all through, and fields-in-traits provide a cleaner solution for many cases, but actually an inference based solution brings many advantages by virtue of never discussing fields, ala no Self::x etc. They'd work in associated types too. |
@burdges I added this RPC as alternatives and mention it. My proposal of partial(-access) types just uses what already is in Rust. And as a result we have a mathematical guarantee that it is safe parallel using variables as using by sequence. |
We know three approaches to this problem (1) explicitly named fields like this proposal, (2) expose fields via some trait mechanism, or (3) infer the fields somehow. Of these, (2) and (3) work for traits, but afaik (1) makes no sense for traits, which makes proposals like this one not too useful in practice. I think (2) lacked any inference originally, so no it did not require a "cleaver compiler" originally, but not sure exactly how https://github.com/nikomatsakis/fields-in-traits-rfc evolved later. An inference solution ala (3) requires a "cleaver compiler" but likely not so much different than what's being done anyways. In essence, "access filters" become relative lifetimes, aka lifetime modifiers relative to In practice, we'd typically have two or three methods which require simultaneous mutable borrows, so this looks roughly like
We need no knowledge of the fields when using this trait, but our trait requires the methods It typically matters more what borrows outlive a method than what actually goes into the You cannot so easily distinguish "what I need now" vs "what I need later" if you focus upon the fields more explicitly, ala (1) or (2), because those fields no longer have meaning to the outgoing borrowed types I'm convinced the compiler could always infer these relative lifetimes correctly without help from the programmer. If I'm wrong then you could imaging aiding the compiler, like:
We've now come back to roughly your access patterns, except they describe what borrows outlive method bodies better, and they appear more readable ala separate lines, etc. |
@burdges My proposal is honest and it is minimal. So, based on this, I know for example, that FT-RFC still needs multi-selfs if we need several functions which read common field, but write different fields: pub fn mf1_rfc(&mut self1 : &mut %{field1, %any} Self, &self2 : & %{common, %any} Self)
{ /* ... */ }
pub fn mf2_rfc(&mut self1 : &mut %{field2, %any} Self, &self2 : & %{common, %any} Self)
{ /* ... */ } |
@clarfonthey Since you understand core idea of partial types and that this idea is minimal and best solution, we could cooperate together about this proposal. Partial types in theory works with any Product Type ( Main idea - to give to compiler a mathematical guarantee that it is safe parallel using variables as using them by sequence |
This proposal is hard to read, most reactions are "confusions". 💖 ThanksTo @clarfonthey for their reviews of the draft proposal. |
This RFC proposes Partial Types as universal and type safe solution to "partial (not) borrowing" like problems.
Rendered
where
%full
and%{..}
are type access;%permit
and%deny
are field access;%min
and%max
are access filters;%any
and%cut
are quasi-fieldsThis is an alternative to Partial borrowing issue#1215, View patterns internals#16879, Permissions #3380, Field projection #3318, Fields in Traits #1546, ImplFields issue#3269