Idea: "Access" trait and member offset


#1

Inspired by @withoutboat 's post, especially the “Offset-based solutions” sections, I come up with an idea as the following:

use std::fmt::Display;

// In std or core
trait Access<'a,Src>: Copy {
    type Tgt: 'a;
    fn access<'b>(&self, src: &'b Src) -> Self::Tgt;
}
impl<'a, Src, Tgt, F> Access<'a,Src> for F
    where F: (FnOnce(&Src) -> Tgt) + Copy,
          Tgt: 'a
{
    type Tgt = Tgt;
    fn access<'b>(&self, src: &'b Src) -> Tgt {
        (*self)(src)
    }
}

//user code
//sugar signature as
//fn print<S,T>(a:impl S::as T, s:&S) where T: ?Sized + Display
fn print<'a,S,T>(a:impl Access<'a,S,Tgt=&'a T>, s:&S)
where T: ?Sized + Display + 'a
{
    println!("{}", a.access(s))
}

fn main() {
    let v = (1,"Hello, World!",true);
    //sugar as:
    //print(_::1,&v);
    print(|v:&(_,&'static str,_)| v.1,&v);
    
    struct MyStruct {
        field: &'static str
    };
    let v = MyStruct{ field: "Good bye, World!" };
    //sugar as:
    //print(_::field,&v);
    print(|v:&MyStruct| v.field,&v);
}

You can run the above in playground.

This is similar to C++'s member offset feature.

Things to clarify:

  • @withoutboats didn’t prefer this compare to Pin, I have no objection on this.

  • This is NOT an alternative to the proposed Pin type. In my opinion Pin type (which cannot move any more) serves different purposes than the above “Access in need” and have different use cases, although they can both be used to create self referencing data.

  • I am currently using similar thing in my interpreter - to present the self-referencing code (you will need recursion/loops, right?) interpreting.

  • Pin seems requires some language support and so need to wait for stabilization. My proposal is pure desugaring (and maybe improvement on type inference) and didn’t really add anything new. Even without the desugaring, adding Access (maybe AccessMut variant as well) to std is an improvement I think, because in my use cases I have custom instances of Access to something inside Vecs or HashMaps etc.

Now I am not sure the above should convince you that we should have this thing RFCed?


#2

See my proposal sketch. I think my solution is much cleaner, since it makes field offsets into real types that can be used anywhere. There’s no need to use closures as a proxy for this, since closures that project a field can always be inlined. In fact, your proposal will probably result in binary bloat due to having to monomorphize for every field! (Mind, mine has a similar problem, but it’s only a problem per field type.)

Actually, it doesn’t. Pin only requires the Unpin trait, which is implemented as an auto trait, an std-only feature currently- but certainly not Pin-specific.


#3

The difference of your proposal and mine

  • We used different grammar, which I have minimal concern of.
  • I also proposed a custom trait to capture similar semantics, which will be useful in different use cases.

So yes, my proposal can be added on top of yours in a way that S.T : for<'a> Access<'a, S,Tgt=&'a T> + AccessMut<'a,S,&'a mut T>.

To avoid binary bloat, we can implement only for fn type (without specilization this will prevent the user from implementing on FnXXX traits), the compiler can then convert the closures into fn pointers and so have the same type per field, as in your proposal.


#4

Unfortunately, that’s not much better. If you use fn pointers, you now turn field lookups, which would normally just be offset movs, into calls, which are very expensive in comparison. In principle, the linker should flatten out the binary bloat both approaches produce, but your approach seems a bit messier. Since it’s sugar, it also opens up the possibility that someone passes an invariant-violating closure into your function; this means that it’s unsafe to use this trait with Pin, just like Pin::map is unsafe.


#5

Seems like another analog of Copy/Clone pair. We need Access for non-trivial field access (in my use cases, I have Vec<T>: Access<'a,Vec<T>,Tgt=Option<&'a T>>VecOffset<T>: Access<'a,Vec<T>,Tgt=Option<&'a T>>); and we may also need an Offset trait for trivial field access marker…


#6

I think that conflating the elements of a vector with the vector’s fields (which are separated by a layer of indirection) is a bad idea. v[0] is nothing like a field access. Field accesses are meant to be compile-time and always correct. I think your use-case is better served by a custom trait in your crate, plus an implementation for T.U that does the obvious thing.

I’ll point out that your example constraint is completely wrong. You can’t use a Vec<T> to access a Vec<T>. You want usize: for<'a> Access<'a, Vec<T>, Target = Option<&'a T>>, which, if you ask me, is just silly.


#7

Ahh… you are right. This IS what I was doing (actually a newtyped usize), just a mistake in my previous post…

Is it? If I just use usize everywhere it might seems like so; but a newtyped usize ensure I will never accidentally mass them up with arithmetic operations… Yet I still need Access to ensure it serves its purpose.

This is why I call it Access, not Offset.

Another analog of Copy?

Analog of Clone:wink:


#8

If talking about AccessMut:

pub trait AccessMut<'a, Source>: Copy  {
    type Target: 'a;
    fn access_mut(&self, src: &mut Source) -> Self::Target;
}

Then yes, because Source is passed by mutable reference it could be partially moved; But this is not the case for Access: you are not able to do partial move from Source, as you only have an immutable reference.

Furthermore, I don’t think there is ANY proposal that can make mutable field access safe for Pin: as long as you enable the user to obtain &mut T from your struct, you are allowing the user to move that field out of the struct. This is not a Pin object supposed to allow you to do.