nikomatsakis / fields-in-traits-rfc

An (experimental) RFC repo devoted to the "fields in traits" RFC.
Apache License 2.0
65 stars 1 forks source link

Infer fields using relative lifetimes #20

Open burdges opened 1 year ago

burdges commented 1 year ago

We maybe have independent reasons for doing fields-in-traits, but they appear unnecessary for typical "partial borrowing" use cases.

We de facto loose lifetime elision with any partial borrowing scheme because you'll typically have methods that several fields but for different lifetimes and with different mutability. We should lean into this by expressing only disjointness using only "relative lifetimes". I think formally these resemble:

impl Trait for Type {
    type 'a<'self> where 'self: 'a<'self> = { field1, field2 };
}

In other words, relative lifetimes are associated lifetime constructors in the trait, or inherent type, which construct a lifetime that outlives 'self and represents only a subset of the fields of the underlying type.

We could infer the above complex syntax by simply (a) declaring disjointness for specific named relative lifetimes and (b) explicitly using these relative lifetimes in methods. All this resembles:

trait Trait {
    disjoint 'a, 'b, 'c;
    fn foo(&'a mut 'c self) -> Foo<'a>; 
    fn bar(&'b+'c mut self) -> &'b mut Bar; 
}

This says: At least three disjoint sets of fields 'a, 'b, 'c exist, all possibly empty. fn foo borrows 'c immutably and 'a mutably, but its return only borrows 'a mutably. fn bar borrows 'b and 'c mutably, but its return only borrows 'b mutably.

In other words, foo and bar could both be called sequentially since both abandon their overlapping 'c borrow, but neither could be called a second time until you abandon its return. Also, you cannot invoke foo and bar from simultaneous unrelated borrows. In particular, you cannot have separate conventional FnMuts built from foo and bar, unless you somehow make those FnMut traits be sub-traits of Trait.

Any impl Trait for Type causes rustc to infer which fields actually belong under 'a, 'b, 'c, based upon its actual implementations of foo and bar, but our above rules for invoking foo and bar have become part of the trait. It's possible for 'c to be inferred to empty, but we do not leak this emptiness so you still cannot invoke foo and bar from simultaneous unrelated borrows.

Advantages? We all love real fields but often you want them abstracted somehow, ala RefCell vs Mutex or Vec<T> vs [T; N]. A bunch of disjoint getter etc methods likely captures this better, ala fn get_fieldA(& 'fieldA self) -> impl Deref<Target=Whatever> + 'fieldA. It's already lower notation overhead, but you could lower this further by making disjoint method_name, ...; be sugar for disjoint 'method_name, ...; plus fn method_name(&'method_name [mut] self, ... It also works uniformly in traits and inherent types and likely plays nicer in semver.

burdges commented 3 weeks ago

We'd likely want some mechanism by which you could promise that a supertrait could not conflict too, but I'm unsure if the same relative lifetime "algebra" siffices there.