Why is i32::from(f) not allowed but 'f as i32' is?

I have some code which converts between f64 and i32, I want to make more generic (type T instead of i32). Most of the methods were easily converted, however I hit a snag here:

non-primitive cast: f64 as T
note: an as expression can only be used to convert between primitive types. Consider using the From trait

So I tried to switch from as i32 to i32::from, but it isn't allowed:

fn main() {
    let f: f64 = 0.0;
    let a: i32 = f as i32; // works
    let b: i32 = i32::from(f); // fails
}

Fails with the error:

the trait std::convert::From<f64> is not implemented for i32

Why isn't it implemented? I also cannot implement it myself due to, as I understand it, Rust's orphan rules (which prevent you from implementing external traits for external types). as does what I want, but I couldn't find any trait to represent a type that can be as'd.


To make it concrete, here is the code I'm trying to convert:

use std::convert::From;
use std::fmt;

#[derive(Clone, Copy)]
pub struct FixedPoint32(i32);

impl From<f64> for FixedPoint32 {
    fn from(x: f64) -> Self {
        FixedPoint32((x * 32.0) as i32)
    }
}

impl From<FixedPoint32> for f64 {
    fn from(x: FixedPoint32) -> Self {
        x.0 as f64 / 32.0
    }
}

impl fmt::Debug for FixedPoint32 {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "FixedPoint32({} = {})", self.0, f64::from(*self))
    }
}

fn main() {
    let f = FixedPoint32::from(1.0);

    println!("f = {:?}", f);
}

Here's my broken attempt at making the code generic. It almost works, the first two methods compile but I can't seem to use it due to the missing From<f64> trait implementation on i32, nor implement the fmt method:

use std::convert::From;
use std::fmt;

#[derive(Clone, Copy)]
pub struct FixedPoint<T>(T);

impl<T: From<f64>> From<f64> for FixedPoint<T> {
    fn from(x: f64) -> Self {
        FixedPoint::<T>(T::from(x * 32.0))
    }
}

impl<T: From<f64>> From<FixedPoint<T>> for f64 where f64: From<T> {
    fn from(x: FixedPoint<T>) -> Self {
        f64::from(x.0) / 32.0
    }
}

impl<T> fmt::Debug for FixedPoint<T> where T: fmt::Display {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        //write!(f, "FixedPoint32({} = {})", self.0, f64::from(*self))
        Ok(())
    }
}

fn main() {
    let f: FixedPoint<i32> = 1.0.into();

    println!("f = {:?}", f);
}

fails with the same error:

error[E0277]: the trait bound `i32: std::convert::From<f64>` is not satisfied
  --> /private/tmp/foo2.rs:27:34
   |
27 |     let f: FixedPoint<i32> = 1.0.into();
   |                                  ^^^^ the trait `std::convert::From<f64>` is not implemented for `i32`
   |
   = help: the following implementations were found:
             <i32 as std::convert::From<bool>>
             <i32 as std::convert::From<i16>>
             <i32 as std::convert::From<i8>>
             <i32 as std::convert::From<std::num::NonZeroI32>>
           and 2 others
   = note: required because of the requirements on the impl of `std::convert::From<f64>` for `FixedPoint<i32>`
   = note: required because of the requirements on the impl of `std::convert::Into<FixedPoint<i32>>` for `f64`

Am I missing something, is there a workaround for From not allowing f64 to i32 conversion, or a better way to accomplish what I'm trying to do here?

1 Like

This is because you cannot losslessly convert from f32 to i32, which is what i32::from means. The as keyword truncates a f32 into an i32.

For similar reasons you can't use From to convert from an i32 to a f32 because there are numbers you can't represent losslessly as a f32 that you can represent as an i32.

To work around this you could create your own trait to convert from a f32.

1 Like

The as operator, when used to convert between primitive types, is something of a brute-force tool. It mostly tries to do the "obvious" thing, at the expense of not handling edge cases well. For instance, the f32 as i32 conversion by default truncates (rounds towards zero), but what about values larger than what could fit in i32? Or NaN and infinity values? Well, right now those cases are actually not even implemented properly and cause undefined behavior!

The From and Into conversions, to the contrary, must never fail, and should also be lossless, so that if you do a roundtrip conversion, you should get back the same value as the one you started with. This means a f32-to-i32 conversion, intrinsically fallible and lossy, cannot be implemented in terms of From and Into.

1 Like

Thank you @RustyYato and @jdahlstrom, makes sense, I suspected the something along those lines.

Got it working with a custom trait I call Fromf64:

use std::convert::From;
use std::fmt;

pub trait Fromf64 {
    fn from_f64(f: f64) -> Self;
}

impl Fromf64 for i32 {
    fn from_f64(f: f64) -> Self {
        return f as i32;
    }
}

#[derive(Clone, Copy)]
pub struct FixedPoint<T>(T);

impl<T: Fromf64> Fromf64 for FixedPoint<T> {
    fn from_f64(x: f64) -> Self {
        FixedPoint::<T>(T::from_f64(x * 32.0))
    }
}

impl<T: Fromf64> From<FixedPoint<T>> for f64 where f64: From<T> {
    fn from(x: FixedPoint<T>) -> Self {
        f64::from(x.0) / 32.0
    }
}

impl<T> fmt::Debug for FixedPoint<T> where T: fmt::Display, f64: From<T>, T: Fromf64 + Copy {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let x: f64 = (*self).into();
        write!(f, "FixedPoint32({} = {})", self.0, x)
    }
}

fn main() {
    let f: FixedPoint<i32> = FixedPoint::<i32>::from_f64(1.0);

    println!("f = {:?}", f);
}

This works, but seemed ugly and inelegant, like I was working against the language. I feel I already have too many of my own traits to workaround the orphan rules (a Lengthable to convert to/from usize, including bool). Fortunately I found this very handy crate, num-traits. It includes a NumCast trait that does basically what I want, and its from method returns an Option, resolving the undefined behavior with as. I can unwrap() this option and it will safely panic instead.

Here is what I came up with using num-traits, works great:

use std::convert::From;
use std::fmt;
use num_traits::cast::{cast, NumCast};

#[derive(Clone, Copy)]
pub struct FixedPoint<T>(T);

impl<T: NumCast> From<f64> for FixedPoint<T> {
    fn from(x: f64) -> Self {
        let n: T = cast(x * 32.0).unwrap();
        FixedPoint::<T>(n)
    }
}

impl<T: NumCast> From<FixedPoint<T>> for f64 {
    fn from(x: FixedPoint<T>) -> Self {
        let f: f64 = cast(x.0).unwrap();
        f / 32.0
    }
}

impl<T> fmt::Debug for FixedPoint<T> where T: fmt::Display, f64: From<T>, T: NumCast + Copy {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let x: f64 = (*self).into();
        write!(f, "FixedPoint({} = {})", self.0, x)
    }
}

fn main() {
    let f: FixedPoint<i32> = 1.0.into();
    println!("f = {:?}", f);

    let g: FixedPoint<i8> = 1.0.into();
    println!("g = {:?}", g);

}
1 Like

I suggest a somewhat longer, more descriptive, less potentially misleading trait name such as LossyFromF64. The problem with using FromFoo is that someone else who tries to read or maintain your code will expect FromFoo to meet the generic "lossless" requirements of from traits, which your trait does not do.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.