Home > Software engineering >  Generic function for multiplying by a constant number
Generic function for multiplying by a constant number

Time:09-20

I am trying to understand generic functions. I know this topic has been discussed extensively, but I can't seem to wrap my head around some details.

I am attempting something similar to this, but even simpler. I just want a double function that multiplies the argument by two and I want it to work for any float or integer type. So far I have this:

use std::convert::From;
use std::ops::Mul;

fn double<A>(x: A) -> A
where A: Mul<Output = A>   From<f32> {
    return x * A::from(2f32);
}

This works fine, when I call it with a float (f32 or f64):

println!("{:?}", double(2f64));

This gives me 4.0.

But it fails for any integer type, for example:

printl!("{:?}", double(2i32));

This gives the following error:

println!("{:?}", double(2i32));
                 ------ ^^^^ the trait `From<f32>` is not implemented for `i32`

I understand the error. There is no general way to cast a float to an integer. It's ambiguous how that is supposed to work, if I wanted to cast 2.5 inside my function instead of 2.0.

I know there is the TryFrom trait, but I cannot figure out, how to make use of it in this situation.


I tried it the other way around, i.e. requiring A to implement From<u32> for example, but this just switches the error from occurring with integers to occur with floats. But what surprised me is that it also leads to an error when I call the function with a signed integer like 2i32.

What am I missing? How can I realize a generic double function over any number type?

CodePudding user response:

Since you just need to convert the number 2 to whatever type, why not use From<u8>?

use std::ops::Mul;

fn double<A>(x: A) -> A
where
    A: Mul<Output = A>   From<u8>,
{
    return x * A::from(2);
}

fn main() {
    println!("{}", double(1));
    println!("{}", double(1f32));
    println!("{}", double(1f64));
}

Playground

The only downside of this simple approach is that double(1i8) will fail to compile (but it will work with i16, i32, etc.). This is because From<u8> is not implemented for i8 because there are some u8 values that cannot be represented as i8. However, we know we really don't need to represent all u8 values as A, but just 2u8. That's why we want to use TryFrom:

fn double<A>(x: A) -> A
where
    A: Mul<Output = A>   TryFrom<u8>,
{
    x * A::try_from(2).unwrap_or_else(|_| unreachable!())
}

The unwrap will never panic for numeric types because there is no numeric type that cannot represent the number 2.

CodePudding user response:

From<f32> means that any f32 value can be converted to the target type without loss. This obviously can't be done for i32 since all non-integer values would lose their fractional part in the conversion.

Conversely, From<u32> can't be done for f32 because f32 only have 24 bits of mantissa, so any value greater than 2^24 would loose precision in the conversion. From<u32> also can't be done for i32 since values greater than 2^31 can't be represented.

Since the only value you're interested in converting is 2, you can use From<i8> or From<u8>, but note that it will still fail for the other one:

use std::ops::Mul;

fn double<A>(x: A) -> A
where A: Mul<Output = A>   From<i8> {
    return x * A::from(2);
}

fn main() {
    println!("{}", double(1));
    println!("{}", double(1f32));
    println!("{}", double(1f64));
    // println!("{}", double(1u8));    // Doesn't work
}

Playground

  • Related