Proposal
Problem statement
Currently the default way when converting between integer types is using as, if into() doesn't do what you want. However so many behaviors are encoded in this one little word. There long have been calls for dedicated functions which encode intent better.
Some recent work has gone into this problem:
But I think this is not enough.
-
I think what is missing is a saturating conversion between all integer types, full stop. There is no reason this should be limited to specific pairs of types, the conversion is always infallible and unambiguous.
-
What is technically not entirely missing (due to the existence of TryInto and unwrap/unwrap_unchecked) but would be nice to have is checked/strict/unchecked conversion between all integer types, full stop. There is no reason this should be limited to specific pairs of types, the conversion is always unambiguous and the error condition (out-of-bounds) is always the same.
The reason for this is two-fold:
- the lack of type ascription and it not coming any time soon makes using
TryInto awkward.
TryInto is too generic and can be more than just a numeric conversion.
Motivating examples or use cases
Honestly, these conversions are so ubiquitous and uncontroversial I don't think they need further motivation, especially the saturating_cast which has no viable alternative for a lot of data type pairings currently. That said, the motivation of the checked_cast API over try_into is again two-fold:
-
It's just a lot nicer. E.g. compare these two:
// Today:
let signed_len: i64 = ...;
let len: usize = signed_len.try_into().unwrap();
array[len / 2]
// With this proposal:
let signed_len: i64 = ...;
array[signed_len.strict_cast::<usize>() / 2]
-
(Unsafe) code can not comfortably rely on it being a numeric conversion. E.g. in foo(x.try_into().unwrap()) there's no direct way to know without inspecting foo what the try_into might do, and the definition of foo might change after inspection. With this proposal you rest assured that foo(x.strict_cast()) is always a numeric conversion.
Solution sketch
I propose the following set of functions to be added to all primitive integer types (both signed and unsigned):
mod convert {
trait CheckedCastFromInt<Int> : SealedCast<Int> {
fn checked_cast_from(value: T) -> Option<Self>;
unsafe fn unchecked_cast_from(value: T) -> Self;
fn strict_cast_from(value: T) -> Self;
}
trait BoundedCastFromInt<Int> : SealedCast<Int> {
fn wrapping_cast_from(value: T) -> Self;
fn saturating_cast_from(value: T) -> Self;
}
}
impl T {
/// Converts `self` to the target integer type, saturating at the nearest edge
/// of the target type's domain if the value does not lie in within it.
fn saturating_cast<T: BoundedCastFromInt<Self>>(self) -> T;
/// Converts `self` to the target integer type, wrapping around at the
/// boundary of the target type.
fn wrapping_cast<T: BoundedCastFromInt<Self>>(self) -> T
/// Converts `self` to the target integer type, returning `None` if the value
/// does not lie in the target type's domain.
fn checked_cast<T: CheckedCastFromInt<Self>>(self) -> Option<T>;
/// Equivalent to `self.checked_cast::<Int>().unwrap()`.
fn strict_cast<T: CheckedCastFromInt<Self>>(self) -> T;
/// Equivalent to `self.checked_cast::<Int>().unwrap_unchecked()`.
unsafe fn unchecked_cast<T: CheckedCastFromInt<Self>>(self) -> T;
}
The implementations of the BoundedCastFromInt and CheckedCastFromInt traits are not shown here. They ought to be implemented for all combinations of primitive integer types. These traits are considered an implementation detail as a permanently-unstable sealed trait. A later proposal may consider stabilizing it for the use in generics, but I consider that out-of-scope for this ACP. I do not think this trait should ever be unsealed, so that unsafe code can rely on it.
Open questions
-
Should cast also be added? It would be implemented whenever the cast is infallible. This overlaps with the unstable widen, but can also cross signedness (e.g. infallibly convert u8 to i16). If added, should widen be removed, or should it remain a method that's only implemented when the conversion is actually widening within the same signedness? Alternatively, should widen be the name for what would-be cast and simply allow sign conversion?
-
Should the NonZero types be included? To allow for this the saturating_cast and wrapping_cast are on a dedicated trait, as a * -> NonZero<Int> saturating/wrapping cast are problematic.
-
Should overlapping unstable API's be removed? truncate is very similar to wrapping_cast although it is always implemented. checked_signed_cast and such are covered by checked_cast, however type inference with the _signed_cast variants can be better.
Alternatives
-
As mentioned above, there is some overlap with other proposed/unstable APIs, which means they're technically an alternative. However in my opinion those are unnecessarily restrictive in the type combinations they support, when the proposed operations are wholly unambiguous/uncontroversial to support on the full spectrum of types.
For example, take checked_truncate and checked_cast_unsigned. If I'm checking that the numeric conversion is successful anyway, does it really matter whether the would-be-invalid conversion was a truncation or a signed -> unsigned conversion? I think it adds unnecessary friction in picking the 'correct' function when it doesn't matter in either the outcome or code pattern.
Similarly take saturating_truncate and saturating_cast_unsigned, if I'm saturating on the supported domain anyway, do we really need two separate APIs for a saturating truncation, and a saturating signed conversion?
Plus, even the combination of all those above APIs still doesn't cover something as simple and useful as a saturating conversion of i64 to usize.
A two-step saturating/wrapping conversion between signed and unsigned types is very dangerous. After consulting multiple expert programmers, more often than not they get it wrong.
-
TryInto is an alternative to checked_cast, but is less convenient due to a lack of type ascription, and can be dangerous because TryInto can be implemented by anyone for anything. checked_cast is always between integers.
Links and related work
An implementation of this ACP can be found at https://docs.rs/int-cast/.
Proposal
Problem statement
Currently the default way when converting between integer types is using
as, ifinto()doesn't do what you want. However so many behaviors are encoded in this one little word. There long have been calls for dedicated functions which encode intent better.Some recent work has gone into this problem:
extend/truncate*_cast_signed/*_cast_unsignedininteger_cast_extrasBut I think this is not enough.
I think what is missing is a saturating conversion between all integer types, full stop. There is no reason this should be limited to specific pairs of types, the conversion is always infallible and unambiguous.
What is technically not entirely missing (due to the existence of
TryIntoandunwrap/unwrap_unchecked) but would be nice to have is checked/strict/unchecked conversion between all integer types, full stop. There is no reason this should be limited to specific pairs of types, the conversion is always unambiguous and the error condition (out-of-bounds) is always the same.The reason for this is two-fold:
TryIntoawkward.TryIntois too generic and can be more than just a numeric conversion.Motivating examples or use cases
Honestly, these conversions are so ubiquitous and uncontroversial I don't think they need further motivation, especially the
saturating_castwhich has no viable alternative for a lot of data type pairings currently. That said, the motivation of thechecked_castAPI overtry_intois again two-fold:It's just a lot nicer. E.g. compare these two:
(Unsafe) code can not comfortably rely on it being a numeric conversion. E.g. in
foo(x.try_into().unwrap())there's no direct way to know without inspectingfoowhat thetry_intomight do, and the definition offoomight change after inspection. With this proposal you rest assured thatfoo(x.strict_cast())is always a numeric conversion.Solution sketch
I propose the following set of functions to be added to all primitive integer types (both signed and unsigned):
The implementations of the
BoundedCastFromIntandCheckedCastFromInttraits are not shown here. They ought to be implemented for all combinations of primitive integer types. These traits are considered an implementation detail as a permanently-unstable sealed trait. A later proposal may consider stabilizing it for the use in generics, but I consider that out-of-scope for this ACP. I do not think this trait should ever be unsealed, so thatunsafecode can rely on it.Open questions
Should
castalso be added? It would be implemented whenever the cast is infallible. This overlaps with the unstablewiden, but can also cross signedness (e.g. infallibly convertu8toi16). If added, shouldwidenbe removed, or should it remain a method that's only implemented when the conversion is actually widening within the same signedness? Alternatively, shouldwidenbe the name for what would-becastand simply allow sign conversion?Should the
NonZerotypes be included? To allow for this thesaturating_castandwrapping_castare on a dedicated trait, as a* -> NonZero<Int>saturating/wrapping cast are problematic.Should overlapping unstable API's be removed?
truncateis very similar towrapping_castalthough it is always implemented.checked_signed_castand such are covered bychecked_cast, however type inference with the_signed_castvariants can be better.Alternatives
As mentioned above, there is some overlap with other proposed/unstable APIs, which means they're technically an alternative. However in my opinion those are unnecessarily restrictive in the type combinations they support, when the proposed operations are wholly unambiguous/uncontroversial to support on the full spectrum of types.
For example, take
checked_truncateandchecked_cast_unsigned. If I'm checking that the numeric conversion is successful anyway, does it really matter whether the would-be-invalid conversion was a truncation or a signed -> unsigned conversion? I think it adds unnecessary friction in picking the 'correct' function when it doesn't matter in either the outcome or code pattern.Similarly take
saturating_truncateandsaturating_cast_unsigned, if I'm saturating on the supported domain anyway, do we really need two separate APIs for a saturating truncation, and a saturating signed conversion?Plus, even the combination of all those above APIs still doesn't cover something as simple and useful as a saturating conversion of
i64tousize.A two-step saturating/wrapping conversion between signed and unsigned types is very dangerous. After consulting multiple expert programmers, more often than not they get it wrong.
TryIntois an alternative tochecked_cast, but is less convenient due to a lack of type ascription, and can be dangerous becauseTryIntocan be implemented by anyone for anything.checked_castis always between integers.Links and related work
An implementation of this ACP can be found at https://docs.rs/int-cast/.
integer_cast_extrasrust#154650