Make a crate generic for available float types

I've got something that looks like it could result in what I want, but it could mean throwing a macro around almost all code in every file within the crate:

struct ConstBase<T: 'static + Float> { phantom: PhantomData<&'static T> }
macro_rules! define_constants {
    ( $( $ftype:tt ),* ) => { $(
        impl ConstBase<$ftype> {
            pub(crate) const PI: $ftype = std::$ftype::consts::PI;
            pub(crate) const METRIC_G: $ftype = 9.80665;
        }
    )* }
}
define_constants!(f32, f64);  // plus f16b, f128, whatever else

That piece is then accessible by ConstBase::<f64>::PI or ConstBase::<f32>::PI, which looks like how I'd expect a generic float type struct to work.

But I'm still having issues connecting type T to know it should expect to be able to find f64 / f32 / etc. implementations, such as in:

pub(crate) struct Const<T: Float> {
    pub pi: T,
    pub g: T,
}

impl<T: Float> Const<T> {
    fn new_metric() -> Self {
        Const {
            pi: ConstBase::<T>::PI,
            g: ConstBase::<T>::METRIC_G,
        }
    }
}

The compiler cannot connect the struct of generic T: Float so that it knows it has f32 and f64 implementations of ConstBase it can use. The solution seems to be using that same form of macro again around that second impl block. And then again around every impl block for any struct of generic T to hard code f32 and f64 paths for EVERYTHING. Not a great solution...

It would be nice if this could be handled by something cleaner, along the lines of derive/attribute macros. But I'm happy to abandon this direction if it's too unwieldy, will cause problems further on, or just plain won't work...

For now defining constants as f64 and converting them into the runtime value seems like a possible solution.
Assuming your Float trait has a fn from_f64(v: f64) -> Self, then you can use pi: T::from_f64(f64::consts::PI).

The optimizer will take care doing the conversion at compile time when a known value is used.

I’m assuming, you are using num_traits::float::Float.

Consider defining pi as acos(-1) and using NumCast trait for g.

This doesn’t allow you to define actual const values, but the compiler should (I hope) make it efficient nonetheless.

use num_traits::float::Float;
use num_traits::cast::NumCast;

#[derive(Debug)]
pub struct Const<T: Float> {
    pub pi: T,
    pub g: T,
}
impl<T: Float> Const<T> {
    fn new_metric() -> Self {
        Const {
            pi: T::acos(-T::one()),
            g: NumCast::from(9.80665).unwrap(),
        }
    }
}

fn main() {
    println!("{:?}", Const::<f64>::new_metric());
    println!("{:?}", Const::<f32>::new_metric());
}

(playground)

And for some approach closer to what you were doing: Consider defining a trait.

use num_traits::float::Float;

pub trait FloatWithConsts: Float {
    const PI: Self;
    const METRIC_G: Self;
}
macro_rules! define_constants {
    ( $( $ftype:tt ),* ) => { $(
        impl FloatWithConsts for $ftype {
            const PI: $ftype = std::$ftype::consts::PI;
            const METRIC_G: $ftype = 9.80665;
        }
    )* }
}
define_constants!(f32, f64);

#[derive(Debug)]
pub struct Const<T: FloatWithConsts> {
    pub pi: T,
    pub g: T,
}
impl<T: FloatWithConsts> Const<T> {
    pub fn new_metric() -> Self {
        Const {
            pi: T::PI,
            g: T::METRIC_G,
        }
    }
}

fn main() {
    println!("{:?}", Const::<f64>::new_metric());
    println!("{:?}", Const::<f32>::new_metric());
}

(playground)

2 Likes

Oh nice, nifty ideas here! Thank you! And yes, num_traits::Float is the trait I was using.

Also, for reference, the alternative solution I'd like to avoid is:

#[cfg(target_pointer_width = "32")] type FloatType = f32;
#[cfg(target_pointer_width = "64")] type FloatType = f64;

within lib.rs. Because that means compiling for each desired float type.

AFAIK pointer width has nothing to do with size of floats.

1 Like

Ah, then #[cfg(feature = "f32")] or something to that effect. It was more as a concept, than the final proven implementation.

Though target_pointer_width does correctly highlight the f64 branch and drop the f32 branch for my machine. I assumed compile flags would allow selection of that per compile. But, whether it's a direct connection or just a convenient overlap, I'm happy as long as it works.

But, well now, that's an interesting thought...

[features]
floats = [ "f32", "f64", "f128" ]

Would be a nice option to directly specify which floats it should work with. Then the question is: How do I get those float feature names into macro calls, or otherwise use them to control crate float types?
I suppose this works:

#[cfg(feature = "f32")] macro_call!(f32);
#[cfg(feature = "f64")] macro_call!(f64);

There's also num_traits::FloatConst.

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.