Macro argument overlap / input reduction

I'm looking to set up a simple macro with the following form:

macro_rules! constants {
    ($type: ty, $name: ident) => {
        pub(crate) mod $name {
            pub(crate) const PI: $type = std::$name::consts::PI;
        }
    }
}

It is pretty nice and straightforward. But is there a way to assign $name based on the input $type (or vice versa). It would be nice to not require the same text input to be written twice, getting constants!(f64, f64); to reduce to just constants(f64);?

I think the next step would be allowing for inputting of a series of types, so constants!(f32, f64); But the best I can get to so far is:

macro_rules! constants {
    ( $( $type:ty, $name:ident ),* ) => {
        $(
            pub(crate) mod $name {
                pub(crate) const PI: $type = std::$name::consts::PI;
            }
        )*
    }
}

constants!(f32, f32, f64, f64);

You can use a tt macro argument in both places, but it will restrict you to unqualified names:

macro_rules! constants {
    ($name: tt) => {
        pub(crate) mod $name {
            pub(crate) const PI: $name = std::$name::consts::PI;
        }
    }
}
1 Like

Ah! Here you are pointing me in the correct direction, yet again. Many thanks!

So does "single token tree" mean it's a single defined value that will be applied as different possible branches based on the expected use? (Ahh, this helped, a bit)

Token tree is:

  • either a single token (identifier, literal, punctuation),
  • or a sequence of token trees placed in delimiters (they may be "invisible" delimiters from the previous macro expansion, or the brackets).

Then, token tree can be inserted literally anywhere. If this insertion is invalid, it will be found when checking syntax of the expanded code.

1 Like

Note that in this example you could have used :ident:

macro_rules! constants {(
    $( $name:ident ),* $(,)?
) => (
    $(
        pub(in crate)
        mod $name {
            pub(in crate)
            const PI: $name = ::std::$name::consts::PI;
        }
    )*
)}

Also note that there was a way to XY your problem here by using a use to re-export:

macro_rules! constants {(
    $( $name:ident ),* $(,)?
) => (
    $(
        pub(in crate)
        mod $name {
            pub(in crate)
            use ::std::$name::consts::PI;
        }
    )*
)}

Finally, you could invert f32::PI and use PI::f32, so as to be able to define others consts with, maybe, some other macro, and not have the conflict that f32 has already been defined:

macro_rules! constants {(
    $( $name:ident ),* $(,)?
) => (
    #[allow(nonstandard_style)]
    pub(in crate)
    mod PI {
        $(
            pub(in crate)
            use ::std::$name::consts::PI as $name;
        )*
    }
)}

and just for the kicks, here is a version using . instead of :: :

macro_rules! constants {(
    $( $name:ident ),* $(,)?
) => (
    #[allow(nonstandard_style)]
    pub(in crate)
    struct PI {
        $(
            $name: $name,
        )*
    }
    pub(in crate)
    const PI: PI = PI {
        $(
            $name: ::std::$name::consts::PI,
        )*
    };
)}

fn main ()
{
    assert_eq!(PI.f64 as f32, PI.f32);
}
3 Likes

Oh, nice. Thanks for the input. I'm aiming to have the final use look like Const::<f64>::new() resulting in a Const<f64>. I'm wary of having separate f64 and f32 implementations as namespaces that are disconnected from the base type they contain. I think that could lead to issues type checking everything where two separate entities of a given type interact. Eventually it would be nice if things were automatically able to interact using casting from one to the other in the case of differing types. But, to start with, I'd hope that when using the crate to create Item<f64>s, they would then just happily work directly with any OtherItem<f64>.
Here's another discussion relating to this question.

/// Helper trait describing the functionality of Self = fN.
pub
trait f32_or_f64 {
    /* here you can put all the functionality that you'd like to have for Const<T> */
    const PI: Self;
}

macro_rules! impls {(
    $(
        $fN:ident
    ),+ $(,)?
) => (
    $(
        impl f32_or_f64 for $fN {
            const PI: Self = ::std::$fN::consts::PI;
        }
    )+
)}
impls!(f32, f64);

/// This type is a proxy type that acts as a module.
///
/// It is as if this were a _generic module_.
pub
struct Const<fN> (
    ::core::marker::PhantomData<fN>,
)
where
    fN : f32_or_f64,
;
impl<fN> Const<fN>
where
    fN : f32_or_f64,
{
    const PI: fN = <fN as f32_or_f64>::PI;
}

fn main ()
{
    dbg!(Const::<f32>::PI);
    // dbg!(Const::<u32>::PI); /* error[E0277]: the trait bound `u32: f32_or_f64` is not satisfied */
}
1 Like

Oh... no... it can't be that simple.... num_traits::Float was a red herring, making me think I'd need to implement all the trig, zero, sign, abs, etc, etc, etc to connect it to the standard floats...

All I really needed was to attach an empty trait to whatever floats I wanted!?!?

This in lib.rs:

impl FloatTypes {}
#[cfg(feature = "f32")] impl FloatTypes for f32 {}
#[cfg(feature = "f64")] impl FloatTypes for f64 {}

Just magically makes this work:

pub struct Const<T: FloatTypes> {
    test: T,
}

impl<T: FloatTypes> Const<T>
    fn new() -> Self {
        test: ConstBase::<T>::SOME_CONST,
    }
}

Happily linking T to either f32 or f64. I knew there was something stupid and obvious I was missing!

1 Like

The trait may be empty or not, depending on the usage you do in a fully generic context. In my example, for instance, we can do, in a generic <fN : f32_or_f64> context, Const::<fN>::PI (whether being able to support that is useful is another question).

If all the usages of your Const wrapper will be happening with concrete types, then you can indeed just use en empty trait (marker trait / filet), or even get rid of the trait altogether (although in that case people may "by accident" feed weird types to Const such as Const::<()> which may lead to an error message more obscure than the trait bound `(): f32_or_f64` is not satisfied).

Well, I'm hitting another wall... Perhaps @2e71828 or @Yandros would be willing to take another look at this?

If I change from this:

pub(crate) trait Floats {}
#[cfg(feature = "f32")] impl Floats for f32 {}
#[cfg(feature = "f64")] impl Floats for f64 {}

to this:

pub(crate) trait Floats: Copy + Debug + PartialOrd {}
#[cfg(feature = "f32")] impl Floats for f32 {}
#[cfg(feature = "f64")] impl Floats for f64 {}

it breaks the connection between Floats and f32 / f64.

I could leave off all the additional traits in Floats and add them in by hand everywhere they're needed. But I think that would just be punting the problem elsewhere. Anything for type T: Floats + std::cmp::PartialOrd (or any further traits) would end up with the same issue of not being able to know it has f32 and f64 implementations.

The other direction I had been considering was to try for a blanket T for trait implementation like this:

pub(crate) trait FloatTraits: Copy + PartialOrd + Debug {}
impl<T: Copy + PartialOrd + Debug> FloatTraits for T {}

I even tried adding the original Floats in to that mix. I also tried (separately and together) to explicitly implementing the group of traits specifically for f32 and f64 as in:

pub(crate) trait FloatTraits: Floats + Copy + PartialOrd + Debug {}
#[cfg(feature = "f32")] impl FloatTraits for f32 {}
#[cfg(feature = "f64")] impl FloatTraits for f64 {}

Whatever it is that I'm missing, it's preventing the compiler from knowing T should be considered as a floating point number, so numbers fail outright (0.0 results in 'expected type parameter T, found floating-point number'). I suppose that could be solved by going to T::from(0.0).unwrap(), but it's just masking the underlying problem.

I'll also add that I recognize the impl<T: Copy + PartialOrd + Debug> FloatTraits for T {} would probably grab in a lot more of the primitive types, so innately loses that 'floating point numbers'-only quality. But then (as far as I can reason) the following should re-restrict it back to just 'floating point numbers' by looping in only types that meet the Floats trait:

pub(crate) trait Floats {}
#[cfg(feature = "f32")] impl Floats for f32 {}
#[cfg(feature = "f64")] impl Floats for f64 {}

pub(crate) trait FloatTraits: Copy + PartialOrd + Debug {}
impl<T: Floats + Copy + PartialOrd + Debug> FloatTraits for T {}

How so?

For just the simple constants struct I get this for every field:

'^^^^^^^^ associated item not found in constants::Const<T>'

And also, just in general let x: T = 0.0 results in:

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

I don't really see what code that you had before compiling using trait Floats {} no longer compiles when you add : Copy + Debug + PartialOrd superbounds :grimacing:

Anyways, my previous playground does compile when you add the superbounds, and you can also add an associated ZERO or _0 constant (whichever name you prefer) so that:

fn check<fN : f32_or_f64> ()
{
    let _0: fN = Const::<fN>::ZERO; // or `fN::ZERO` directly
    assert!(_0 < Const::<fN>::PI);
}

compiles fine.

1 Like

Oh wow.... I think this piece: <fN as f32_or_f64>::PI is the secret sauce I needed!!

Setting impl<T: IsFloat> didn't actually allow floats as T, but it does mean that the ability is there, I just have to use it correctly!

1 Like

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.