Projections that do not decide mutability up front are possible

I encountered the following problem while working on a transpiler from non-Rust code to Rust. Suppose that I have these structs:

#[derive(Debug, bytemuck::TransparentWrapper)]
#[repr(transparent)]
struct Foo(pub i32);

struct Bar {
    pub x: i32,
    pub y: i32,
}

Suppose that I want to pretend that Bar has fields of type Foo as much as it has fields of type i32, that is, be able to write an expression that:

  • is a place expression,
  • denotes the field x or y of a Bar, in the same way that bar.x and bar.y do,
  • has the type Foo, not i32 (which is sound because of the repr(transparent)), and
  • can be borrowed as mutable or shared without deciding while writing the expression.

After presuming for a while that this is impossible (after all, conventional Rust APIs have all these separate *_ref() and *_mut() functions, which must be there for a reason), I realized today that there is in fact an overloadable operator in Rust that offers this opportunity: the indexing operator.

use std::ops;
use bytemuck::TransparentWrapper;

struct X;
struct Y;
impl ops::Index<X> for Bar {
    type Output = Foo;
    fn index(&self, X: X) -> &Self::Output {
        TransparentWrapper::wrap_ref(&self.x)
    }
}
impl ops::Index<Y> for Bar {
    type Output = Foo;
    fn index(&self, Y: Y) -> &Self::Output {
        TransparentWrapper::wrap_ref(&self.y)
    }
}
impl ops::IndexMut<X> for Bar {
    fn index_mut(&mut self, X: X) -> &mut Self::Output {
        TransparentWrapper::wrap_mut(&mut self.x)
    }
}
impl ops::IndexMut<Y> for Bar {
    fn index_mut(&mut self, Y: Y) -> &mut Self::Output {
        TransparentWrapper::wrap_mut(&mut self.y)
    }
}

These definitions let this sort of code compile:

fn simple_example() {
    let mut bar = Bar { x: 0, y: 0 };
    bar[X] = Foo(1);
    bar[Y] = Foo(2);
    println!("{:?} {:?}", bar[X], bar[Y]);
}

fn more_specific_borrow_checked_usage(input: &mut Bar) {
    // Can have multiple borrows (must not use `&mut input`)
    let x1 = &input[X];
    let x2 = &input[X];
    println!("{x1:?}, {x2:?}");
    // Can mutate (must not use `&input`)
    let x3 = &mut input[X];
    *x3 = Foo(10);
    assert_eq!(input.x, 10);
}

It will even work on foreign types:

impl ops::Index<X> for [i32; 2] {
    type Output = Foo;
    fn index(&self, X: X) -> &Self::Output {
        TransparentWrapper::wrap_ref(&self[0])
    }
}
// ...

I haven't yet used this trick (I only just thought of it while I was cleaning out some old draft posts) and I may or may not actually use it, but I thought it was worth sharing.

6 Likes

P.S. I should have said that “infallible projections …” are possible. For example, you can't merge Option::as_ref() and Option::as_mut() with this trick (unless the chosen output type allows you to return an &None of sorts).

Very clever. But I also feel it is too clever. It violates the principle of least surprise.

There is a good argument to be made for following patterns and not being overly clever in code. (For code for serious purposes that is. If you are doing recreational coding, code golfing etc: go for it!)

1 Like

It would be less surprising if the index operator was implemented for a single enum Axis { X, Y } type instead of multiple nominal ZSTs.

1 Like

That could work, but only for uniform types on the fields. Or you would need an enum for the Output type as well, which doesn't sound ergonomic.

Oh, certainly. But, my use case is allowing a very un-clever code generator to still produce working Rust — nobody should be looking at its output except for debugging.

Another use case would be: it could be generated by a macro which is intended to function as a place expression. In that case, the macro cannot write the _ref()/_mut() properly since it has no context. (But of course, a reasonable choice in many cases would be to have two macros instead.)

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.