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 T
is 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. TryFrom
doesn'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
isize
andusize
is 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
Cast
trait (likeInto
) try_cast
andtry_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).