Generic type for float and integer with simple arithmetic ops support

I have a program where I need to implement the same logic to plot a struct of data. Something like this:

struct TimeData {
    x_data: Vec<i64>,
    y_data: Vec<f32>,
}

where the x-axis here represents time stamps, and also I need to plot fourier frequency data, that has a similar struct:

struct FFTData {
    x_data: Vec<f32>,
    y_data: Vec<f32>,
}

There's more information than these simple xy data, so I have a "Plottable" trait that represents things that can be plotted:

trait Plottable<XAxisType: num_traits::Num> {
    fn get_first_x_point(&self) -> Option<&XAxisType>;
    fn get_last_x_point(&self) -> Option<&XAxisType>;
    fn get_x_axis<'a>(&'a self) -> &'a Vec<XAxisType>;
    fn get_y_axis<'a>(&'a self) -> &'a Vec<DataType>;
}

The plotting function, that takes a generic Vec of plottables shouldn't care whether the x-axis is integer or f32, but rust can't let it go. For example, for a vector of plots, I need to determine the min and max for a collection of plots. So I run this:

    let min = plots
        .iter()
        .filter(|el| el.get_first_x_point().is_some())
        .map(|el| {
            el.get_first_x_point()
                .expect("We filtered for first points")
        })
        .min()
        .ok_or_else(|| {
            Box::new(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                "Failed to get min",
            ))
        })?;
    let range_margin = 0.02f32;

And then... I do the sin of trying to do this:

    let min_m = (1f32 - range_margin) * min;

and everything falls apart. It's like there's no way to make rust understand that I want to do all the casts necessary to make this happen. What's the right way to do this?

Here's the playground link.

My question:

How can I do arithmetic operations on both i64 and f32 with no issues.

Is there a better rustacean way of doing this?

Thank you. I really appreciate your help.

I didn't follow your complete example, but maybe something like:

-let min_m = (1f32 - range_margin) * min;
+let min_m = (1.try_into().unwrap() - range_margin) * min;

I get some more errors (and missing type annotation) when I apply that fix to your code, though. But perhaps it's a start.


Floats don't implement Ord because of the special role of NaN (which isn't equal to itself and not smaller or greater than any other float). I had to work around this using sort_by and/or sort_by_key. Maybe you would need to define a trait that provides a comparison functions for the numeric types you use.

Sorry, but you missed the problem. (1f32 - range_margin) is not the issue at all. The multiplication is the issue.

Then maybe add the .try_into().unwrap() after the closing parenthesis?

There's no method "try_into()" for f32, according to the compiler.

Are you using Rust Edition 2021 or an older Edition? See Edition Guide. You might need to manually import TryInto with:

use std::convert::TryInto;
  1. But I believe I can give it a try to upgrade. Do you believe that would make a difference?

See edit above. You can also write use std::convert::TryInto;

That doesn't work. I also tried into() by itself and that has the same type-annotation problem. None of that works because the compiler doesn't know how to convert f32 to XDataType and vice-versa (and hence the question I'm asking here). Please try it in the playground. The type annotation that was missing will give you the proper error when you attempt to specify min_m type to be f32 or XDataType.

I try, but I get this:

-    // let min_m = (1f32 - range_margin) * min;  // <--- the sin
+    let min_m: f32 = (1f32 - range_margin).try_into().unwrap() * min;

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0425]: cannot find value `range_margin` in this scope
  --> src/main.rs:74:30
   |
74 |     let min_m: f32 = (1f32 - range_margin).try_into().unwrap() * min;
   |                              ^^^^^^^^^^^^ not found in this scope

For more information about this error, try `rustc --explain E0425`.
error: could not compile `playground` due to previous error

Is that what you meant?

Apologies. I missed that definition of range_margin. It's just an f32. Here's the corrected link. I'll update the question as well:

This is as far as I get:

-    // let min_m = (1f32 - range_margin) * min;  // <--- the sin
+    fn infer<T>(_dst: T, value: T) -> T { value }
+    let min_m = infer(*min, (1f32 - range_margin).try_into().unwrap()) * *min;  // <--- the sin

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0277]: the trait bound `XDataType: From<f32>` is not satisfied
  --> src/main.rs:77:51
   |
77 |     let min_m = infer(*min, (1f32 - range_margin).try_into().unwrap()) * *min;  // <--- the sin
   |                                                   ^^^^^^^^ the trait `From<f32>` is not implemented for `XDataType`
   |
   = note: required because of the requirements on the impl of `Into<XDataType>` for `f32`
   = note: required because of the requirements on the impl of `TryFrom<f32>` for `XDataType`
   = note: required because of the requirements on the impl of `TryInto<XDataType>` for `f32`
help: consider further restricting this bound
   |
56 |     XDataType: num_traits::Num + Ord + std::convert::From<f32>,
   |                                      +++++++++++++++++++++++++

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground` due to previous error

I did lots of similar gymnastics, and eventually reached a dead-end, which is why I'm asking this question here.

I know this question makes me look like an idiot... but I'm supposedly an old developer who have been in the business for over 10 years and been doing rust for 4 years. I miss SFINAE in C++...

It seems like there are things that can't be learned in rust.

I have often felt stupid when I couldn't achieve very simple tasks in Rust. Being abstract over numeric types doesn't seem trivial to me.

Maybe someone else can help?

This should work: Playground.

Necessary changes:

  • Use PartialOrd instead of Ord bound + Use reduce instead of min
  • Use num_traits::cast::ToPrimitive bound to cast min to f32

Additional changes:

  • Use filter_map instead of filter + map

Thank you! This looks like an appropriate solution!