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 IntCast<Int> : Sealed<Int> { ... }
}
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<Int>(self) -> Int where Self: IntCast<Int>;
/// Converts `self` to the target integer type, returning `None` if the value
/// does not lie in the target type's domain.
fn checked_cast<Int>(self) -> Option<Int> where Self: IntCast<Int>;
/// Equivalent to `self.checked_cast::<Int>().unwrap()`.
fn strict_cast<Int>(self) -> Int where Self: IntCast<Int>;
/// Equivalent to `self.checked_cast::<Int>().unwrap_unchecked()`.
unsafe fn unchecked_cast<Int>(self) -> Int where Self: IntCast<Int>;
}
The implementation of IntCast trait is not shown here. It ought to be implemented for all combinations of primitive integer types. This trait is 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.
Then, I propose the removal of the following unstable functions from integer types:
// Removed integer_extend_truncate:
fn saturating_truncate<Target>(self) -> Target where Self: TruncateTarget<Target>;
fn checked_truncate<Target>(self) -> Option<Target> where Self: TruncateTarget<Target>;
// strict_truncate is already absent.
// Note that I'm **not** proposing the removal of the infallible truncate / extend.
// integer_cast_extras (removed entirely)
fn checked_cast_signed(self) -> Option<iN>;
fn saturating_cast_signed(self) -> iN;
fn strict_cast_signed(self) -> iN;
fn checked_cast_unsigned(self) -> Option<uN>;
fn saturating_cast_unsigned(self) -> uN;
fn strict_cast_unsigned(self) -> uN;
These are all directly covered by this proposal.
Open questions
-
Which code pattern should these functions follow?
fn saturating_cast<Int>(self) -> Int where Self: IntCastTarget<Int>;
fn saturating_cast<Int: IntCastFrom<Self>>(self) -> Int;
In this proposal I followed the example of integer_extend_truncate, but it's unclear to me which of these two is better.
-
Should cast also be added? It would be implemented whenever the cast is infallible. This overlaps with the unstable extend, but can also cross signedness (e.g. infallibly convert u8 to i16). If added, should extend be removed, or should it remain a method that's only implemented when the conversion is actually widening within the same signedness?
-
Should wrapping_cast also be added? It overlaps with truncate but follows the saturating_, checked_, wrapping_, etc, pattern better, and allows direct two's complement conversions between signed and unsigned types of different sizes (e.g. i8 to usize conversion). If this is added, should truncate be removed entirely, or should it remain as a method that's only implemented when the conversion is actually narrowing within the same signedness?
-
Should the NonZero types be included? If so saturating_cast and wrapping_cast (if added) need to go on dedicated traits rather than IntCast, as a * -> NonZero<Int> saturating/wrapping cast can not be infallible.
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.
-
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 implementation of
IntCasttrait is not shown here. It ought to be implemented for all combinations of primitive integer types. This trait is 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.Then, I propose the removal of the following unstable functions from integer types:
These are all directly covered by this proposal.
Open questions
Which code pattern should these functions follow?
In this proposal I followed the example of
integer_extend_truncate, but it's unclear to me which of these two is better.Should
castalso be added? It would be implemented whenever the cast is infallible. This overlaps with the unstableextend, but can also cross signedness (e.g. infallibly convertu8toi16). If added, shouldextendbe removed, or should it remain a method that's only implemented when the conversion is actually widening within the same signedness?Should
wrapping_castalso be added? It overlaps withtruncatebut follows thesaturating_,checked_,wrapping_, etc, pattern better, and allows direct two's complement conversions between signed and unsigned types of different sizes (e.g.i8tousizeconversion). If this is added, shouldtruncatebe removed entirely, or should it remain as a method that's only implemented when the conversion is actually narrowing within the same signedness?Should the
NonZerotypes be included? If sosaturating_castandwrapping_cast(if added) need to go on dedicated traits rather thanIntCast, as a* -> NonZero<Int>saturating/wrapping cast can not be infallible.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.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