Question about templates

Hello,

I think a have a problem with understanding templates.

Consider the following method:

    impl Fsm{ 
        pub fn handle_event<T:Ord+Copy+Clone>(&mut self, ev: FsmEvents, user_data: T) -> u8 {
            let mut ev_processed : u8 = 0;

            if user_data>=4.0{
                
            }
...

In main I call the method as follows:

let data : float = received.user_data; 
myfsm.handle_event(received.ev, data);

I get the following error.

error[E0308]: mismatched types
  --> src/fsm.rs:60:27
   |
55 |         pub fn handle_event<T:Ord+Copy+Clone>(&mut self, ev: FsmEvents, user_data: T) -> u8 {
   |                             - this type parameter
...
60 |             if user_data>=4.0{
   |                           ^^^ expected type parameter `T`, found floating-point number
   |
   = note: expected type parameter `T`
                        found type `{float}`

I thought that the compiler generates for each used type of T an implementation exactly for that type. I only use float so where is the problem to do a comparison.

Lets consider I would use a struct with some data as T.
Can I then access the members of the struct from within my template method?

Thank you,
Peter

You are writing "template" so I understand that you might be coming from C++. Rust's generics (the proper name of this feature) are not like C++ templates. The body of a generic function in Rust is typechecked only once, upfront, relying on only the trait bounds and constraints that it explicitly declares in the signature. It is not the case that every instantiation is separately typechecked with the concrete types substituted for type parameters.

This results in a superior user experience and overall more reliable code, since it's not possible to accidentally forget any constraints in the signature, so once your function body typecheks, you can be sure that it will work with any appropriate/intended types. In contrast, C++ templates are hard to write reliably, especially in a library setting, because the lack of type constraints results in "surprise" compile errors that manifest only when you actually try to instantiate the template with a given set of type substitutions.

7 Likes

Thank you for this clarification. Let me describe my design needs. Maybe you can recommend the right rust approach.

The handle_event() method is actually generated. And I want to give the user the option to hand over an own object where data can be made available used e.g. for state transition guards etc. the user as used in the state machine design.

I thought a generic object would be a good idea. Maybe not.

What would be the better language feature to realise that?

In worst case a users has to provide a data type that I use during generation. But I hoped to be able to avoid this.

Thanks,
Peter

If you compare value of type T with a float, you have to have a very explicit bound on the function that it requires types comparable with float. Rust won't let you use T in any way that isn't declared up front in the trait bound. It's almost like writing the whole function twice: once in the <T: …> and the second time in function's body. Rust also forbids implicit type conversion, and requires bounds for these too. Overall, for numeric code the trait bounds can get a bit absurd, so I don't recommend using generics unless you really have to (e.g if f64 is good enough, just go for hardcoded f64).

The Ord bound by default says the type is comparable only with itself, so for example T could be a string, and you can have string > string, but you can't have string > 4.0.

Second problem is that Ord is total order that forbids NaN, and f32 allows NaN, so it will never implement it. You need PartialOrd instead.

fn comparable<T>(data: T) -> bool
    where T: PartialOrd<f32> {
    data > 4.0_f32
}

Another option would be to require a type that can be converted to float:

fn comparable<T>(data: T) -> bool
    where T: Into<f32> {
    data.into() > 4.0_f32
}

This is usually the easiest option, since you can call into() once and the rest of the function can be non-generic, so won't need extra bounds.

6 Likes

If you do find you need to be generic over numeric types, you'll probably want to look at the num crate. However, I agree that explicit types (the one you need or a handful via macros) is the better choice most of the time.

3 Likes

Could you instead define a trait

trait AsFloat {
  fn as_float(&self) -> f64;
}

pub fn handle_event<T: .... + AsFloat>(... user_data: T) .. {
   if user_data.as_float() >= 4.0 { ... }
}

If you need float, this seems the most straight forward

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.