What trait counts as {float} or 'floating-point number'?

So Julia has this capability:

struct Example{T <: AbstractFloat}
    field::T = 0.0
end

Which then defines a struct that knows it can have type f16, f32, or f64.

an::Example{f32} = Example(0.0)
another = Example(0.0) # Defaults to f64

Perfect!

But I have spent several weeks battling in rust trying to get the same effect. This seems to be EXACTLY the purpose of generic types and traits. But they just don't functionally work to know that 0.0 can be T because T is a {float} or "floating point number"

It seems like you should be able to do something similar in rust: But this results in

^^^ expected type parameter T, found floating-point number

The only "solution" I've found is to wrap EVERY impl<T: Float> Test<T> block that uses floats... so like all of them... into a macro to double every one to run both impl Test<f32> and impl Test<f64> blocks. Basically this via macro.

I keep thinking there should be a better solution. I literally just want to be able to say:

pub struct Test<T = f32 || f64> { field: T }
impl<T = f32 || f64> Test<T> { 
    // Code that uses floats without resorting to NumCast/unwrap for each, such as:
    const: T = cast(0.0).unwrap();
}

That's because nothing in Rust tells it that Float is really just f32 or f64. It could just as well be implemented for i32, YourCustomStruct and () and Rust needs to assume that it does.

It you want to go that way, you need to be fully generic and not use the 0.0 literal, but actual trait methods, e.g. as provided by num-traits:

use num_traits::{Zero, Float};

pub struct Test<T> {
    pub field: T,
}

impl<T: Float + Zero> Test<T> {
    fn new() -> Self {
        Test {
            field: Zero::zero(),
        }
    }
}

pub fn main() {
    let new_struct = Test::<f64>::new();
    println!("The new struct has a field value of {}", new_struct.field)
}

Playground

4 Likes

So is there no way to tell rust that T can be ONLY an f32 or f64?

And I don't want just zero. I want any arbitrary float I use, such as 9.80665, to just work.

I mean, I get that I'm fighting for something that may not even be that useful in the long run and I could just plop f64 in for T everywhere and forget about this entirely. But, also, I'm not exactly asking for something extreme here; rust knows that 1.234 is a {float}. But that's only an internally accessible semantic thing it seems...

Correct.

1 Like

Oh, well then.... Is this something that rust is happy to be incapable of doing?

I mean, the other option I've considered is to do something like this in lib.rs:

#[cfg(feature = "f32")] type FloatType = f32;
#[cfg(feature = "f64")] type FloatType = f64;

And then set features to each to compile a different library for each float type, giving f32 and f64 crates. Then those can be wrapped in another crate that just handles the type issues and interfacing...

But, it would be really nice if I could just write one crate that was generic to float types, without having to jump through hoops. I mean, I just want to run a boatload of algebra on a handful of objects. Rust is supposed to be the "stick shift" of programming. I would love the capability to transition to something fancy like SIMD for 32 bit floats for less accuracy-critical entities without having to cobble together some awkward solution.

You can always create your own trait that you only implement for f32 and f64.

2 Likes

I think I tried that as well here, like:

pub trait IsFloat {}
impl IsFloat for f32 {}
impl IsFloat for f64 {}

Well, in that case if you specify the trait bound T: IsFloat, the generic type parameter will only be able to be substituted for f32 and f64, as you want.

Using the Traits from num_traits that @jer already mentioned, you can do:

use num_traits::float::Float;
use num_traits::cast::FromPrimitive;

pub struct Test<T> {
    pub field: T,
}

impl<T: Float + FromPrimitive> Test<T> {
    fn new() -> Self {
        Test {
            field: T::from_f32(9.81).unwrap(),
        }
    }
}

pub fn main() {
    let new_struct = Test::<f64>::new();
    println!("The new struct has a field value of {}", new_struct.field)
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e637d6ccbd1148c370daa7796c4a0d20

1 Like

So that does truly and correctly let rust know that T can only be an f64 or f32. At least that is good to know. I've seen the concept presented as T: Trait1 + Trait2 + Trait3 makes a Venn diagram where only the types at the center of it work as T.

But it's a uni-directional thing, so it can't also know that when given numbers it would ALWAYS be able to use them?

Because the second step in my original form was to define a second trait that then brought in Zero, One, PartialEq/Ord, Copy, Debug, etc. etc. and added those to IsFloat resulting in a FloatTraits that was only for floats and called in all of the functionality they need. Except that I can't make it know that it's type T is actually a floating point number.

For convenience you can do something along these lines (and add more dependent Traits and methods to MyFloat as necessary):

use num_traits::float::Float;
use num_traits::cast::FromPrimitive;

pub trait MyFloat: Float + FromPrimitive {
    fn f32(x: f32) -> Self { Self::from_f32(x).unwrap() }
}

impl MyFloat for f32 {}
impl MyFloat for f64 {}

pub struct Test<T> {
    pub field: T,
}

impl<T: MyFloat> Test<T> {
    fn new() -> Self {
        Test {
            field: T::f32(9.81),
        }
    }
}

pub fn main() {
    let new_struct = Test::<f64>::new();
    println!("The new struct has a field value of {}", new_struct.field)
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=cc48edd00d49a2f84402a98e7f293e7b

Rust generics are always open in the sense that Rust will only compile the code if it is forwards compatible with more implementers of the trait being added in the future.

4 Likes

Hmm, I had tried something in that direction, attaching consts to a T: IsFloat struct using a macro to run the same impl block for <f32> and <f64>. But then other implementations of T: IsFloat won't link to the f32/f64 implementations of that struct, even though it should know that T can only be either one.

And I'd love to just be able to add one line into my lib.rs file and have the capability of an f128 be brought in, perhaps with the option to implement custom solver functions (if desired).

I just want access to whatever {float} is. Whatever can be any f__ primitive, but only an f__ primitive.

You can't. {float} is not a type, nor is it a trait. It's a marker used internally by the compiler when it figures out what kind of float a float literal is.

You have to either use an existing trait or make your own trait.

1 Like

Well dang. That is really disappointing...

You can define an enum that does this:

enum Float { f64(f64), f32(f32) }

But it has drawbacks that make it probably not a good solution for your case:

  • It is always sized so that it can contain the largest possible variant, even if it contains a smaller one
  • Raw floats have to be explicitly wrapped, though this can be made easier by implementing From and Into
  • You’ll need to manually write implementations of the std::ops traits for operator overloading, and they’ll have to explicitly handle type mismatches somehow
  • If you return a Float outside of your library, the caller needs to deal with the possibility that the answer is any of the types.

Heh, yeah, I accidentally found myself starting to write out

match T {
  f32 => {},
  f64 => {},
};

once before I realised what I was doing...

What do you mean by "actually"? What do you mean by "make it know"? What other functionality are you expecting from a trait, apart from calling its methods and relying on the invariants it guarantees?

1 Like

If it could just innately handle let x: T = 4953.25

Or my other white whale:

pub(crate) struct C<T: IsFloat> { _phantom: PhantomData<T> }
impl C<f32> { pub(crate) const THIS: f32 = 3.14159 }
impl C<f64> { pub(crate) const THIS: f64 = 3.14159265 }

pub struct Constants<T: IsFloat> { this: T }
impl<T: IsFloat> Constants {
  fn new() -> Self {
    Constants { this: C::<T>::THIS }
  }
}

Where C is a struct that will/can/should never be created (perhaps even ensured to be private and available only internally within the crate). But when a new Constants<T> is created, such as by Constants::<f32>::new_metric() it would grab the correct version of each constant from C. Or throughout the crate when I need some fraction of PI it'd be available in a struct implementation like impl<T: IsFloat> Env<T> via C::<T>::FRAC_SEVEN_THIRTYSIXTHS_PI

Logically, if T is restricted to only a couple types, and if an implementation of every type exists, then there should be no disconnect where it thinks C::<T> can't be C::<f32> or C::<f64>, the only types that T can actually be...

A functioning, but stupid feeling solution, is to hand code (or macro duplicate) all of those f32/f64 implementation blocks on EVERYTHING to replace ANY impl<T: IsFloat> so that it never sees that T <-> {float} disconnect. Two functional halves brought into a functional whole with a giant divide in the middle that it completely ignores.

macro_rules! impl_both {
  ( $( $ftype:ident ),* ) => { $ (
    impl Constants<$ftype> {
      fn new() -> Self {
        Constants {
          this: C::<$ftype>::THIS,
        }
      }
  )* }
}
impl_both!(f32, f64)