Easy Cast
Introducing: easy-cast.
Problem: correct casts aren't easy
Let me ask you, have you ever done this?
#![allow(unused)] fn main() { fn make_some_vec() -> Vec<u8> { vec![] } let v: Vec<_> = make_some_vec(); // We need a fixed-size type (not usize), or just want to save space. // We can assume length <= u32::MAX. let len = v.len() as u32; }
Of course, you could write this instead:
#![allow(unused)] fn main() { fn make_some_vec() -> Vec<u8> { vec![] } let v: Vec<_> = make_some_vec(); // We need a fixed-size type (not usize), or just want to save space. use std::convert::TryFrom; let len = u32::try_from(v.len()).unwrap(); }
What about this?
#![allow(unused)] fn main() { let x: f32 = 1234.5678; // Convert to the nearest integer, assuming it fits let x = (x.round()) as i32; assert_eq!(x, 1235); }
Or this?
#![allow(unused)] fn main() { use rand_distr::{Poisson, Distribution}; let poi = Poisson::new(1e6_f64).unwrap(); let num = poi.sample(&mut rand::thread_rng()); // Note: u64::MAX as f64 rounds to 2^64, so we need to use < not <= here! assert!(0.0 <= num && num < u64::MAX as f64); let num = num as u64; }
Or even this?
#![allow(unused)] fn main() { fn some_value() -> i32 { 0 } let x: i32 = some_value(); let y = x as f32; if y as i32 == x { println!("No loss of precision!"); // WARNING: this test is WRONG since e.g. i32::MAX rounds to 2^31 on cast // to f32, then rounds back to i32::MAX on cast to i32. } }
Summary
It should be easy and safe to convert one numeric type to another, but frequently it's not:
- Using
x as Tis easy but not safe: it may truncate, sign-convert, round, or saturate. (Before Rust 1.45.0 behaviour might even be undefined.) - Using
T::try_from(x).unwrap()is clunky when you expect the conversion to succeed. TryFromdoesn't even support float conversions.- Conceptually, "convert a float to the nearest integer" is a single operation, yet most solutions require separate rounding and conversion steps.
It turns out this is not an easy problem to solve in general. I wrote an RFC on this. Nearly three years and a long discussion later, it's still unsolved.
Existing solutions
The standard library provides:
From, but it only covers portably infallible conversionsTryFrom, but it misses float conversions and expects you to do your own error handling
There are existing libraries on crates.io, yet all of them fall short in some way:
- num_traits::NumCast: API is not extensible to user types; error handling is the user's responsibility; it's a relatively big library if you just want casts
- cast: different API for fallible and infallible
conversions; API around
isizeandusizeis platform-dependent; error handling is the user's responsibility
Introducing easy-cast
Eventually I gave up trying to find a standard solution and wrote my own,
starting with one particular problem (converting between usize and u32
indices in KAS-text, which stores many text and list indices). This tiny
beginning grew into the easy-cast library:
- a
From-like trait,Conv, for all integer conversions - direct support for float-to-int-with-rounding via
ConvFloat - easier usage via the
Casttrait (likeInto) try_castandtry_conv, allowing user error handling on all conversions- and of course a few bug fixes and diagnostic improvements
Examples
Now, we can revisit the above problems using easy-cast:
#![allow(unused)] fn main() { use easy_cast::Conv; fn make_some_vec() -> Vec<u8> { vec![] } let v: Vec<_> = make_some_vec(); // Expect success, panic on failure let len = u32::conv(v.len()); }
#![allow(unused)] fn main() { use easy_cast::CastFloat; let x: f32 = 1234.5678; // Convert to the nearest integer, panic if out-of-range let x: i32 = x.cast_nearest(); }
#![allow(unused)] fn main() { use easy_cast::CastFloat; use rand_distr::{Poisson, Distribution}; let poi = Poisson::new(1e6_f64).unwrap(); let num = poi.sample(&mut rand::thread_rng()); let num: u64 = num.cast_trunc(); }
#![allow(unused)] fn main() { use easy_cast::Conv; fn some_value() -> i32 { 0 } let x: i32 = some_value(); match f32::try_conv(x) { Ok(y) => println!("{x} is exactly representable by f32: {y}"), Err(e) => println!("{x} is not representable by f32: {e}"), } }
For any integer x and any integer or float type T, we get:
T::conv(x)x.cast()with type inferenceT::try_conv(x)andx.try_cast()returningResult<T, Error>
And for any float value x and integer type T we have:
x.cast_nearest(),x.cast_trunc(),x.cast_floor(),x.cast_ceil()T::conv_nearest(x), etc.T::try_conv_trunc(x),x.try_cast_floor(), etc.
Don't need the fuses?
One of the design goals for easy-cast was check everything in Debug builds,
maybe not in Release builds. In a Debug build, or when the always_assert
crate feature is used, cast and conv will panic on inexact conversions.
In Release builds without this flag, the cast reduces to just x as T.
The same is not true for try_cast and try_from which always fail on
inexact conversions.
We may make some adjustments here in future versions, e.g. to use
always_assert by default (but if so, it will be considered a breaking change).