Template types with unknown fields

Consider the following sample code;

fn a_plus_b<T>(t: T) -> i32 {
    t.a + t.b     // <== error[E0609]: no field `a` and `b` on type `T`
}

struct AB {a: i32, b: i32}

fn main() {
    println!("{}", a_plus_b(AB{a:10, b:20})); // <=== won't work
}

The compiler fails when it tries to compile fn a_plus_b because it can't "see" a and b fields on T. A similar construct (where T's fields are not yet know) would work in C++, because templates are 'lazy evaluated/compiled' on concrete 'invocation/use'.

Question 1: I'm guessing the Rust compiler some good reasons to know T when it compiles a_plus_b, though I'm curious what these reasons are and why not lazily evaluate templates?

Question 2: I am also guessing that the only way to make it work is with traits (e.g. trait MyT with methods such as get_a/get_b/get_mut_a/get_mut_b). Doable and but a lot of boiler plate. Is there any other way apart from traits?

Wish: Could be a nice feature if somehow we are able to satisfy the compiler with where clause like (probably in 2030 :slight_smile):

fn a_plus_b<T>(t: T) -> i32 
where 
    T.a: i32,
    T.b: i32,
{
    t.a + t.b  
}

For the author of fn a_plus_b: they can know that if their code compiles, then it will be usable wherever the function's signature (types, generics, and bounds) permits. It can't accidentally have a requirement that was not intended.

For the author of the function call a_plus_b(ab): they know that the function's signature specifies all of the requirements they have to satisfy to use the function, and if they misuse it, the error will be in terms of those documented requirements, not a random field or method that a_plus_b's implementation used.

Neither of these is a reason that there can't be a where bound like “this field exists”; there just isn’t such a feature for now. It's not a high priority to add such a feature because obligating the type to have a field is much more constraining than having a method, so it is generally considered bad design. It would be useful to enable more patterns of borrowing, though.

3 Likes

All though this still involves using a trait for the method, you can use a macro for the body instead of getters and setters.

Rust macros are more like templates in some senses.

2 Likes

Because C++ is a great example of how "duck typing" or "lazy evaluation" (unconstrained parametric polymorphism with post-monomorphization type checking) is a bad idea in general, even if handy at times. Most infamously, it leads to terrible error messages, but even worse, it leads to terribly obscure interfaces where the user has no idea what exactly the API expects from the type parameters, unless documented very diligently. After 25 or so years C++ now finally has concepts "lite", a restricted form of constrained type parameters. In some ways they're more powerful than Rust traits, and in some ways less so.

Rust has has taken the route of parametricity, meaning that everything a function is able to do with a value is plainly visible in the function signature, whether it's of a concrete type or a type parameter.

6 Likes

thanks @kpreid, @jdahlstrom and @quinedot for the explanations and the sample code. Yeah C++ and Rust are different and good in their own ways. Being new to Rust, I find writing performant generic container/collection libraries like hash maps/graphs etc. is quite hard without actually resorting to unsafe/raw-pointers (e.g. hashbrown internal implementation without unsafe is hard to imagine). This is also an area where some of the duck typing/"field constraints"/lazy evaluation etc. would be handy/convenient but not completely necessary.

Another approach is to write your code in terms of a specific struct that has a generic parameter, which gives some assurance that the fields have the semantic meaning you expect:

fn a_plus_b<T>(t: AB<T>) -> i32
where T: Copy + std::ops::Add<Output=i32> {
    t.a + t.b
}

struct AB<T> {a: T, b: T}

fn main() {
    println!("{}", a_plus_b(AB{a:10, b:20}));
}
2 Likes