Index operator given a type

Currently it's not possible to implement Entity-Component pattern using the index operator. It makes sense because types can't be given as values. But is it possible to have that supported in the language?

The use case is to replace something like entity.get::<TComponent>() by entity[TComponent].

There’s no prior art of allowing type parameters in places where values are syntactically typically expected. Why should we change this, how should it work in detail and why should index notation be the first place to do it?

E.g. I’ve seen the desire (or in some form had it myself) to be able to write syntax like entity.get(TComponent), too, mainly motivated by the relatively high amount of syntactic noise that comes with the turbofish and an empty argument list. If ordinary method calls had to be written foo.get::<>(x) instead of foo.get(x), that would feel a bit noisy, too. The downside on the other hand always is that it’s essentially just a form of introducing a new syntax for something that’s already possible, at the cost of making the language larger with limited benefit.

There is no easy way to just shorten the existing syntax. Ideally, the :: would be redundant, as in entity.get<TComponent>(), but that’s an old infamously hard to solve syntax problem (some say “impossible”). And the parantheses are kind-of wanted, because we like to differentiate fields, even though honestly, via Deref, even field expressions can execute arbitrary code. I.e. without the parens it’s entity.get::<TComponent>, and if the turbo-fish was “solved”, too, it’s entity.get<TComponent>, and the noise is no more. Not a trivial thing to actually do though, in case it isn’t obvious.

I wouldn’t be surprised if there was prior discussion on passing-type-like-arguments style syntax. If you (or someone else) knows/finds some, I’d be glad to read some existing arguments and/or designs people have come up with.

2 Likes

I dont think using the same syntax for types and values is a good idea. But you can abuse consts for less noise

use std::marker::PhantomData;

#[derive(Clone, Copy)]
struct IdxMarker<Component>(PhantomData<Component>);


struct TComponent {
    // stuff
}

#[allow(non_upper_case_globals)]
const TComponent: IdxMarker<TComponent> = IdxMarker(PhantomData);

#[derive(Default)]
struct Entity {
    // more stuff
}

impl std::ops::Index<IdxMarker<TComponent>> for Entity {
    type Output = TComponent;
    fn index(&self, _: IdxMarker<TComponent>) -> &Self::Output {
        todo!()
    }
}

fn main() {
    let entity = Entity::default();
    let _component: &TComponent = &entity[TComponent];
}

This does not introduce ambiguity but might lead to less useful error messages.

6 Likes
trait ComponentTag {
    type Comp;
}

fn get<Tag: ComponentTag>(tag: Tag) -> Tag::Comp {
    todo!();
} 

struct MyComponent {
    ...
}

struct MyComponentTag;

// Boilerplate could be hidden inside a macro
impl ComponentTag for MyComponentTag {
    type Comp = MyComponent;
}

let my_comp = get(MyComponentTag);
2 Likes

Prior art: syn uses this trick for peeking.

This can't be done automatically by the language because you can have a type and a value with the same name; they live in different namespaces, differentiated by whether the syntactic context wants a type or a value. If you're in value position and want a type, the syntax is <QPath>; in type position and want a const value, { expr }.

We have a way to pass types as values, and that's PhantomData. Whether the shorter syntax and inferred mutability is worth the extra code is up to you.

1 Like

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