Magic field access syntax in nalgebra

nalgebra has a Matrix type which acts as the foundation of a plethora of types (N-dimensional vectors, points, etc.). The components of these can be accessed using 'field notation':

even though no such fields appear to exist in these types.

This leads to all sorts of puzzling phenomena, such as

type Point = ncollide3d::math::Point<f32>;
let p: Point = Point::new(0.0, 0.0, 0.0);
let x = p.x;             // `Point` appears to have field `x` ...
let Point { x, .. } = p; // ... except that it doesn't!
error[E0026]: struct `OPoint` does not have a field named `x`
   --> /tmp/fubar/src/main.rs:10:21
    |
 10 |         let Point { x, .. } = p;
    |                     ^
    |                     |
    |                     struct `OPoint` does not have this field
    |                     help: `OPoint` has a field named `coords`

What is this dark magic?

(If this were Python, this magic could be implemented with properties, but I'm not aware of anything like it in Rust.)

Some more puzzling behaviour: rust-analyzer firmly believes (both in the documentation it shows when hovering over new in the above code, and in the error messages it generates) that Point::new has one parameter, rather than 3. But 3-argument calls to Point::new compile, while 1-argument calls do not compile.

Bizarrely enough, when passing only 1 argument to new, rust-analyzer changes its mind, and complains that new requires 3 arguments rather than 1. Could this be related to whatever magic is being used in these types?

It's probably Deref. It's a questionable design choice on their part. Matrix deref to X, which does have a property x. Matrix in fact derefs to multiple different things, which makes this very questionable.

Point derefs to OVector which is a type definition of Matrix. That's a whole lot of deref going on.

Though I admire that whole bit of cleverness with all those const generics to encode information at compile time, I believe they're what causing you trouble with the different new constructors.

4 Likes

Actually, it's the other way around. Point is a type definition of OPoint:

pub type Point<T, const D: usize> = OPoint<T, Const<D>>;

and OPoint itself derefs to X etc.

But there's still a bit of the puzzle I'm missing. For deref to kick in, I need to make some attempt to deref something. In the case of p.x deref would be dereffing p: OPoint to X, but I don't see why that deref is being activated at all.

Field access via . has autoderef.

Example.

3 Likes

Right, but your example explicitly creates a (multiple) ref to s, so there is something to deref.

My situation is more like this, which still works even though there is no reference anywhere.

But, OK, the fact that Deref is implemented for S, makes s dereferencable. I thought that this behaviour was restricted to the dereferencable object appearing after a *, but the docs you linked state that it also applies when it appears before a ..

And it also works with methods. Of course it does, I knew this already ... at least for explicit &s. But it works for Deref in general.

I think that's cleared it up. Thanks.

Moral of the story: both *s and s. will go looking for impl Deref for S.

Yeah, it's in general. You've definitely used it on methods even if you weren't aware of it -- many String methods are actually on str, many Vec<T> methods are actually on [T], etc. And you've probably used it for fields elsewhere too, like going through a Box or Arc. It's less intuitive on a struct that exists for more reasons than thinly wrapping a generic type. But being applied generally is more consistent.

    let s = Arc::new(Box::new(S { t: T { f: () }}));
    println!("{:?}", s.f);
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.