Skip to content
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 (v3) #3736

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

VitWW
Copy link

@VitWW VitWW commented Dec 6, 2024

This RFC proposes Partial Types as universal and type safe solution to "partial borrowing" like problems.
This RFC is a third try and it is based on Partial Types (v2) #3426

Rendered

Example:

struct StructABC { a: u32, b: i64, c: f32, }

// function with partial parameter Struct
fn ref_a (s : & StructABC.{a}) -> &u32 {
    &s.a
}

let s = StructABC {a: 4, b: 7, c: 0.0};

// partial expression, partial reference and partial argument
let sa = ref_a(& s.{a});

@ehuss ehuss added T-lang Relevant to the language team, which will review and decide on the RFC. T-types Relevant to the types team, which will review and decide on the RFC. labels Dec 6, 2024
```
Partiality: .{ PartialFields* }
PartialFields: PermittedField (, PermittedField )* ,?
PermittedField: IDENTIFIER | TUPLE_INDEX
Copy link
Member

@programmerjake programmerjake Dec 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if you had:

struct Foo {
    a: i32,
    bar: Bar,
}

struct Bar {
    b: f32,
    c: String,
}

and you wanted to borrow both a and bar.b but not bar.c?

I think extending partial references to allow this would be useful:

impl Foo {
    fn baz(&self.{a, bar.b}) {
    }
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I'll update this proposal to allow sub-partiality:

Minimal Partiality we could write:

Partiality:      .{ PartialFields* }
PartialFields:   PartialField (, PartialField )* ,?
PartialField:    PermittedField
PermittedField:  IDENTIFIER | TUPLE_INDEX

If we wish to describe partial structs with partial structs inside, we must have a bit more complex Partiality:

PartialField:    PermittedField Partiality?

@compiler-errors
Copy link
Member

I don't believe this RFC sufficiently explains the interaction between partial types and subtyping. I believe introducing new subtyping for things that aren't lifetimes is quite hazardous to Rust due to problems with inference and existing interactions with TypeId.

@VitWW
Copy link
Author

VitWW commented Dec 6, 2024

@compiler-errors

I don't believe this RFC sufficiently explains the interaction between partial types and subtyping.

Could you explain with more details?

My vision:
We just ask the Compiler to mark as error the safe code if we try to read from or to write into fields, that are not allowed to have access.
We already allow to have:

struct StructABC { a: u32, b: i64, c: f32, }

let s = StructABC {a: 4, b: 7, c: 0.0};

let sa = & s.a;
let sb = & s.b;
let sc = & s.c;

let rs = & s;

With this proposal I suggest to allow also:

let sa = & s.{a}; // same as let sa = &s (a: u32);
let sb = & s.{b}; // same as let sb = &s (b: i64);
let sc = & s.{c}; // same as let sc = &s (c: f32);

let sab = & s.{a,b}; // same as let sab = &s (a: u32, b: i64);
let sbc = & s.{b,c}; // same as let sbc = &s (b: i64, c: f32);
let sac = & s.{a,c}; // same as let sac = &s (a: u32, c: f32);

let rs = & s.{a,b,c}; // same as let rs = & s;

@burdges
Copy link

burdges commented Dec 6, 2024

As I'v observed before, rust could infer partial types using special lifetime annotations on traits methods:
nikomatsakis/fields-in-traits-rfc#20
https://internals.rust-lang.org/t/infered-fields-for-partial-borrowing/18766

Inferred partial types have vastly less syntax than any other partial borrowing scheme. You never name fields explicitly, but instead name groups of fields using an implicitly higher order lifetime, for which rustc infers the fields. In essence, subtyping gets done using implicitly higher order lifetimes.

@programmerjake
Copy link
Member

programmerjake commented Dec 7, 2024

As I'v observed before, rust could infer partial types using special lifetime annotations on traits methods: nikomatsakis/fields-in-traits-rfc#20 https://internals.rust-lang.org/t/infered-fields-for-partial-borrowing/18766

an additional benefit of that scheme -- since it uses abstract field groups (lifetimes for that proposal), it works great even if you can't name fields due to visibility or the actual type being behind a generic (e.g. in traits) so you don't know what the field names are. Additionally, for visibility, I think we shouldn't be trying to punch holes through to allow naming private fields, since private field names are supposed to be an implementation detail and we should be allowed to change/add/delete private fields as we see fit without breaking downstream users.

@TimNN
Copy link
Contributor

TimNN commented Dec 7, 2024

I think it would be good for this RFC to talk a bit about visibility.

My assumption is that to be able to write Type.{x} or variable.{x} the field x must be visible.

But can a method pub fn foo(self: &mut Self.{x}) be called even if x is not visible at the call site?

@VitWW
Copy link
Author

VitWW commented Dec 7, 2024

@TimNN

I think it would be good for this RFC to talk a bit about visibility.

Ok, I'll try to write more clearly in RFC about this.

My assumption is that to be able to write Type.{x} or variable.{x} the field x must be visible.

Why? When you write let rs = & s; do you create a reference to struct with visible fields or without?
variable.{x} means not variable.x, but variable with no access to all fields but x. Sure access to this x field is also limited by visibility

@TimNN
Copy link
Contributor

TimNN commented Dec 7, 2024

I'm primarily concerned about the cross-crate use case: if there's a crate alpha with a pub struct Foo { bar: ... }, then alpha can (currently) change the contents of Foo at any time without breaking backwards compatibility, including renaming or deleting bar.

So a crate beta depending on alpha shouldn't be allowed to references bar in any way, because otherwise beta would get broken if alpha changes the definition of Foo.

@VitWW
Copy link
Author

VitWW commented Dec 8, 2024

@TimNN Thank you for the idea to include a part "Unresolved Questions" of RFC as required part.

So a crate beta depending on alpha shouldn't be allowed to references bar in any way, because otherwise beta would get broken if alpha changes the definition of Foo.

Ok, I get it and agree with that.
So we allow to write not just list of "allowed fields only", but another list - "forbidden fields only" in partiality and marked them with off keyword:

    fn baz(foo : & Foo) {
       let fpubs  = &foo.{pubfld1, pubfld2, pubfld3,};
       let fprivs = &foo.{off pubfld1, pubfld2, pubfld3,}; 
       // ....
    }

@AaronKutch
Copy link

AaronKutch commented Dec 12, 2024

My original understanding was that, assuming the fields are private (the situation of course changes if they are public), the rule should be that partial borrows of a struct should only be able to be written by the implementor, and is only observable by the user when using functions from the implementor.

Let's say we had a type

struct Example {
    len: usize,
    storage: Storage<u8>,
}

impl Example {
    pub fn len(&self.{len}) -> usize {
        self.len
    }
    
    pub fn access_fn_with_no_len_change(&mut self.{storage}) -> &mut [u8] {
        ...
    }
}

then the user would be able to

let x = v.access_fn_with_no_len_change();
// then use `len()` as much as you want while mutable borrows are also active
let _ = v.len();
dbg!(x);

The point of having the partial borrow in the signature of the function is that it is something the maintainer has to uphold (with wrappers over raw types, they have to logically uphold it to maintain soundness, but with higher level things the borrow checker will always intervene if a maintainer tries to change the internals to use the wrong sets of borrows, unless they change the signatures but changing the signatures is a breaking change). So the user does not have to worry about breakage.

But, I am seeing now that the user should also have access to the specific sets of borrows available through the public interface, so that when they are writing their own wrapper types and methods over Example they can propogate the abilities.

struct Wrapper {
    example: Example,
    ...
}

impl Wrapper {
    // or is this even expressible with the current proposal?
    pub fn len(&self.{example.{len}}) -> usize {
        self.example.len()
    }
    
    pub fn example_stuff(&mut self.{example.{storage}}) -> &mut [u8] {
        ...
    }
}

The syntax looks like it could get ugly real fast, there has to be some kind of typing or aliasing for sets of lifetimes like a more explicit version of Burdge's proposal. Also, is it possible to allow mixes of mutable and immutable or is that something that fundamentally doesn't make sense? I haven't thought too much about it and am just chiming in on the discussion.

edit: suppose I added

impl Wrapper {
    pub fn len(&self.{example.{len}}) -> usize {
        self.example.len()
    }

    pub fn other_stuff(self.{example.{&len, &mut storage}}) -> &mut [u8] {
        ...
        
        let x = self.example.access_fn_with_no_len_change();
        let _ = v.len();
        dbg!(x);
    }
}
// then `len` can be called with `other_stuff` having live borrows

without something like this partial borrows is useless in some circumstances

@burdges
Copy link

burdges commented Dec 12, 2024

The inferred partial types proposal mixes mutable and immutable using "algebra of relative lifetimes".

trait Trait {
    disjoint 'a, 'b, 'c;

    // mutably borrow 'a fields and return that borrow 
    fn borrow_mut_a(&'a mut self) -> &'a mut A;

    // mutably borrow 'b fields and return that borrow
    // immutably borrow 'a fields, but release after this invokation 
    fn borrow_mut_b(&'b mut 'a self) -> &'b mut B;

    // mutably borrow 'a and 'c fields and return only the 'c borrow
    fn borrow_mut_c(&'c+'a mut self) -> &'c mut C;
}

I suppose traits' relative lifetimes could be exposed via turbofish like

pub fn something<'x, T: Trait+'x>(t: & Trait::<'x>::'b + Trait::<'x>::'a mut T) -> &'a mut A {
    // t.borrow_mut_c() cannot be invoked here
    .. t.borrow_mut_b() ..
    t.borrow_mut_a()
}

Or maybe something nicer than turbofish like

pub fn something<T: Trait>(t: & 'b::<T> + 'a::<T> mut T) -> &'a mut A {

Anyways: Rust code depends heavily upon traits. A trait based approach like inferred partial types facilitates much bigger traits, like those arising in web frameworks, GUIs, DBs, and custom buisness logic.

As a particular scenario, you've developed internal buisness logic mega-trait which makes borrowing awkward, so then you revisit how its methods should be used, and how its impls utilize state, add internal atomics or mutexs where required, and add relative lifetimes so that rustc permits the desired parallel borrowing.

I doubt type & field based approaches really serve many users, especially if being more explicit makes them a syntactic nightmare. Inferred partial types could support enums far more effectively than explicit partial borrowing too, which again helps buisness logic enormously.

@VitWW
Copy link
Author

VitWW commented Dec 13, 2024

@AaronKutch

My original understanding was that, assuming the fields are private (the situation of course changes if they are public), the rule should be that partial borrows of a struct should only be able to be written by the implementor, and is only observable by the user when using functions from the implementor.

Yes, this would be better.

impl Wrapper {
    // or is this even expressible with the current proposal?
    pub fn len(&self.{example.{len}}) -> usize {
        self.example.len()
    }

Is it allowed to use nested partiality? Yes, &self.{example.{len}} is totally Ok.

Also, is it possible to allow mixes of mutable and immutable or is that something that fundamentally doesn't make sense?

In this RFC - no, but in "Future possibilities" - yes, I add describe partial mutable fields.
I even add special Note: For full flexibility of using partial borrowing partial mutability is needed!
Partial mutable stucts are just fully mutable stucts, but it is forbidden to compiler to allow to write something to some "immutable" fields.

pub fn other_stuff(self.{example.{&len, &mut storage}}) -> &mut [u8] { /* .. */ }

No, syntax for partial mutability and partial borrows with mixed mutability looks like this:

pub fn other_stuff(& mut.{example.{storage}} self.{example.{len, storage}}) -> &mut [u8] { /* .. */ }

@FlixCoder
Copy link

When you need to specify the fields at the call-site, isn't this super close to just passing the fields one by one (or grouping them in a new struct of references), which the current type system already allows?

I feel like this approach doesn't give enough of a benefit.

@eira-fransham
Copy link

When you need to specify the fields at the call-site, isn't this super close to just passing the fields one by one (or grouping them in a new struct of references), which the current type system already allows?

I feel like this approach doesn't give enough of a benefit.

A strong motivation for this is disjointness analysis (particularly for mutable references) without needing to make all fields public.

struct SomeStruct {
  a: Foo,
  b: Bar,
}

impl SomeStruct {
  fn a_mut(&mut self.{a}) -> &mut Foo { &mut self.a }
  fn b_mut(&mut self.{b}) -> &mut Bar { &mut self.b }
}

fn some_func(x: &mut SomeStruct) {
  let a = x.a_mut();
  let b = x.b_mut();

  do_something(a, b);
}

Right now, this is impossible without making a and b public since methods require a mutable reference to the whole of SomeStruct and so the compiler doesn't know which fields the methods will access.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC. T-types Relevant to the types team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants