num-complex icon indicating copy to clipboard operation
num-complex copied to clipboard

Generic conversion of real constants into ComplexFloat

Open grothesque opened this issue 8 months ago • 5 comments

In generic complex code it is common to have to convert real values into T: ComplexFloat. For this, I find the following trait very handy:

pub trait IntoComplex<T: ComplexFloat> {
    fn into_complex(self) -> T;
}

impl<T> IntoComplex<T> for f32
where
    T: ComplexFloat,
    f32: Into<T::Real>,
    T::Real: Into<T>,
{
    fn into_complex(self) -> T {
        self.into().into()
    }
}

It allows to write

fn some_calculation<T>(x: T) -> T
where
    T: ComplexFloat + NumAssign,
    f32: IntoComplex<T>,
{
    let a = 2.0.into_complex();
    let b = 3.0.into_complex();
    let mut sum: T = 0.0.into_complex();
    sum += x * a + b;
    sum *= b;
    sum
}

instead of

fn some_calculation<T>(x: T) -> T
where
    T: ComplexFloat + NumAssign,
    f32: Into<T::Real>,
    T::Real: Into<T>,
{
    let a = 2.0.into().into();
    let b = 3.0.into().into();
    let mut sum: T = 0.0.into().into();
    sum += x * a + b;
    sum *= b;
    sum
}

I am aware of num_traits::cast but those casts can fail and they can involve information loss.

In contrast, the IntoComplex conversion always works and preserves all information. This is also why using it involves less boilerplate.

How about adding something along these lines to this crate?

grothesque avatar Apr 30 '25 16:04 grothesque

I just realized that the impl can be made more generic

impl<D, S> IntoComplex<D> for S
where
    D: ComplexFloat,
    S: Into<D::Real>,
    D::Real: Into<D>,
{
    fn into_complex(self) -> D {
        self.into().into()
    }
}

Now it also works for, say, i16 as source type.

grothesque avatar Apr 30 '25 18:04 grothesque

Or perhaps a blanket implementation like impl<S, D: From<S> + Clone + Num> From<S> for Complex<D> would make sense? This might be too generic, though.

grothesque avatar May 05 '25 08:05 grothesque

It can't be that generic because it conflicts with the broad impl<T> From<T> for T -- the compiler sees that it is possible to have S = Complex<D>, no matter that this is unlikely in practice.

Overall, I think it's not a good idea to have multiple implicit conversions, because the compiler will often give up on type inference in that situation. But for your example, I think a simple ComplexFloat::from_real(Self::Real) -> Self method would work pretty well? Then you can write things like:

fn some_calculation<T>(x: T) -> T
where
    T: ComplexFloat + NumAssign,
    i8: Into<T::Real>,
{
    let a = T::from_real(2.into());
    let b = T::from_real(3.into());
    let mut sum: T = T::from_real(0.into()); // or T::zero()
    sum += x * a + b;
    sum *= b;
    sum
}

cuviper avatar May 07 '25 16:05 cuviper

It can't be that generic because it conflicts with the broad impl<T> From<T> for T -- the compiler sees that it is possible to have S = Complex<D>, no matter that this is unlikely in practice.

I see – interesting!

Overall, I think it's not a good idea to have multiple implicit conversions, because the compiler will often give up on type inference in that situation.

Yes, and the double .into() is also too complicated for what it does.

But for your example, I think a simple ComplexFloat::from_real(Self::Real) -> Self method would work pretty well? Then you can write things like:

fn some_calculation<T>(x: T) -> T where T: ComplexFloat + NumAssign, i8: Into<T::Real>, { let a = T::from_real(2.into()); let b = T::from_real(3.into()); let mut sum: T = T::from_real(0.into()); // or T::zero() sum += x * a + b; sum *= b; sum }

This is good. I think that it would be a very useful addition to num-complex!

The IntoComplex trait that I proposed above looks cleaner in use (it’s only a suffix to a constant), but it’s perhaps a case of trait overuse.

grothesque avatar May 13 '25 15:05 grothesque

For now, what do you suggest as a stop-gap measure in real code?

I tried to implement your suggestion as a trait and came up with

pub trait FromReal: ComplexFloat {
    fn from_real(x: Self::Real) -> Self;
}

impl<T> FromReal for Complex<T>
where
    Complex<T>: ComplexFloat,
    i8: Into<T>,
    Self::Real: Into<T>,
{
    fn from_real(x: Self::Real) -> Self {
        Self::new(x.into(), 0.into())
    }
}

But (compared to calling .into().into() or to adding from_real to ComplexFloat) this has the disadvantage of polluting the API of my library with the FromReal trait.

grothesque avatar May 15 '25 09:05 grothesque