Idea: DerefPin/DerefCell

Currently, Pin offers the unsafe map method, which is safe to use when returning a Pin to a struct field or other interior pointer that preserves Pin's constraints. There is the idea of a #[derive]-like macro to automatically generate safe accessor methods to make this nicer.

Similarly, Cell has RFC 1789, which offers a safe conversion from &Cell<[T]> to &[Cell<T>]. This should also apply to struct fields, but there’s no easy way to express this in the type system.

If Pin<'a, T> and &'a Cell<T> were part of the language, these operations could be built in. But that’s a lot of surface area to add to the language. What if we had something like Deref that applied to types like this, where deref returned a *T that the compiler would re-wrap in a Pin/&Cell after field projection or indexing? This could potentially be a much smaller language change to get the same ergonomic wins.

Could such a trait be shared between Pin and &Cell? At first glance it seems the two types of references can be projected into using the same rules. If not, it would need to be two traits, which is not much better than baking Pin and &Cell into the language.

Are there any other reference types that could take advantage of this? &RefCell might be able to use a wide pointer that keeps a reference back to its counter, but I’m not sure how useful that would be. Or is it too specific? I can imagine "map a field projection" working with Option or Result as well, where it wouldn’t be constrained by the same rules as Pin/&Cell.

Thoughts?

7 Likes

This sounds interesting! So essentially this would an an unsafe-to-implement trait that reference types could implement to declare that Ref<'a, T> where T is a struct with a public field f of type U, can be turned into Ref<'a, U> pointing to the field? Really this is a trait that wants to be implemented for type constructors (things of kind Lifetime -> * -> *), not types. I think this needs associated type constructors, and even then it remains somewhat awkward. Something like

trait AccessFieldField {
  type<'a, T> Ref;

  unsafe fn access_field<'a, T, U, F>(
    x: Self,
    map: F
  ) -> Ref<'a, U>
where
  Self = Ref<'a, T>,
  F: FnOnce(*const T) -> *const U;
}

(I know equality constraints are not a thing. Anyways.)

Then x.field would compile to unsafe { x.access_field(|x| &(*x).field as *const _) }.

cell::Ref and cell::RefMut come to my mind.

2 Likes

Does the fact that one of them is a “pointee type” (Cell, which needs an & in addition to make it a reference) and the other is a “pointer type” (Pin, which is already a reference) not throw a wrench into the works?

(In an ideal world, maybe & and &mut themselves would’ve worked this way, instead of autoderef, forming a coherent story together with what we now call “default binding modes”.)

1 Like

Could this be done via some auto-deriving mechanism instead?

I also wonder if it could / should apply to non-reference stuff but arbitrary_self_types cases such as:

@RalfJung Hm, you're right- it does kind of call out for HKT. I wonder if, since it's already compiler magic, whether it would be worth just special casing it? Or if that would be too icky? Maybe split the reconstruction into a separate trait and have the compiler match them up on its own:

trait DerefField { // impl<'a, T> for Pin<'a, T>
    type T;
    unsafe fn deref_field(self) -> *const T;
}

trait RefField { // impl<'a, U> for Pin<'a, U>
    type U;
    unsafe fn ref_field(*const U) -> Self;
}

Despite the ickiness this should work for both Pin<'a, T> and &'a Cell<T>, right?

:smiley: I've liked this idea since the current incarnation of default binding modes was worked out.

That would presumably wind up turning field access into method access, which is why I brought this version up to begin with.

I think the relevant type would be &Cell<T>, which is a pointer. The fact that it is composed of two types should not be a problem, hopefully.

Well but how exactly would the matching-up works?

Also, I was just reminded of the family pattern, and that could maybe even express the right trait without using equality constraints. However, I still have no idea how inference would work:

trait RefFamily {
  type<'a, T> Ref : Deref<Target=T>;
}

struct ShrRef;
impl RefFamily for ShrRef {
  type<'a, T> Ref = &'a T;
}

struct MutRef;
impl RefFamily for MutRef {
  type<'a, T> Ref = &'a mut T;
}

struct CellRef;
impl RefFamily for CellRef {
  type<'a, T> Ref = &'a Cell<T>;
}

struct PinRef;
impl RefFamily for PinRef {
  type<'a, T> Ref = Pin<'a, T>;
}

trait DerefField : RefFamily {
  unsafe fn access_field<'a, T, U, F>(
    x: Self::Ref::<'a, T>,
    map: F
  ) -> Self::Ref<'a, U>
  where
    F: FnOnce(*const T) -> *const U;
}

One would probably have to call this as CellRef::access_field, i.e. there would still have to be magic for determining the family when desugaring an x.f.

1 Like

Some good points on the interaction of field projection with Drop, in the case of Pin, here and here.

If we’re not committed to having the sweetest possible auto-derefy syntax, we could just (“just”?) add a type for “field F in type T” – like C++ member pointers, basically, hopefully without their baggage (which IINM is mostly around member function pointers) – and then each relevant type can just have an inherent method to apply the projection. We don’t need to give the type special syntax like C++ does, but it would probably require language support for constructing instances of it (unless we can and want to hack it together with macros somehow, which, ick).

To illustrate:

pub struct Field<T, F> { offset: usize } 
// (it's an offset, not a size, but fields can't be at negative offsets)

impl<T> Cell<T> {
    pub fn field<F>(&self, field: Field<T, F>) -> &Cell<F> { ... }
}

impl<T> Pin<T> {
    pub fn field<F>(self, field: Field<T, F>) -> Pin<F> { ... }
}

fn example() {
    struct Example { x: i32, y: i32 }
    let cell = Cell::new(Example { x: 10, y: 11 });
    let cell_y: &Cell<i32> = cell.field(.y);
    // (not sure what the precise syntax should be here;
    //  hopefully the compiler could apply some type-directed name resolution)
}

Notably, only the compiler could safely create instances of Field, so you can rely on it being “valid”.

(This could even allow some “mutable destructuring”, with dynamic checks – e.g. from PinMut you might want to get simultaneous sub-PinMuts to two disjoint fields. To support that, we could have an fn that takes two Fields (and returns two PinMuts) and asserts that the extent of the fields, as calculated from their offsets plus sizes_of, don’t overlap. (Mere inequality checks aren’t enough, especially if we allow “deep” Fields into nested structs: e.g. one could be to an outer struct, and another to its second field, overlapping without having the same offset.))

4 Likes

I actually spent a while thinking about this, here: https://internals.rust-lang.org/t/pre-pre-rfc-field-offsets

I think a Field<T, U> (or as I called it, T.U) is the most elegant way to solve this annoying problem for “projectable” smart pointers (@Centril’s Freeze<T> would benefit from this too, as he points out in my thread).

2 Likes

That definitely isolates the compiler magic a lot more effectively than a DerefField trait would! I think the minimum we would need to start experimenting in a library (with a less-nice interface) would be offset_of, right?

1 Like

Not even. You can do a bunch of gross things, though the dropck might decide to kill you. https://play.rust-lang.org/?gist=864666d50278bccdb04857ac36ca9f36&version=nightly&mode=debug&edition=2015

1 Like

However, for Pin at least we’d also like to support projecting through an enum, at which point offsets are not enough any more.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.