-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Traits for lossy conversions #3415
base: master
Are you sure you want to change the base?
Conversation
Bikeshed which sounds fake but that I'm really serious about: that name is too long. |
@Lokathor I changed the name. |
Clarification: the method name is too long, not the PR name :3 |
@Lokathor the section Future possibilities: Inherent methods has a possible solution for that. EDIT: The inherent methods are now the main part of the RFC. |
Deprecating Not to mention, for some time in the future, |
@thomcc I hope that the need for lossy conversions will decrease by making the compiler smarter. For example with this proposal, called "pattern types", the compiler knows the range in which a number is. If this range fits in the type you want to cast to, you can use fn foo(x: u32 is 0..1000) {
let _ = x as u16;
} This means that you don't have to spell long method names in most cases, and you can be more confident that the code is correct, because the compiler checks that |
if it's less terse than |
You do not want truncation to happen in a math expression, do you? Then using |
I most often use |
I think I'll update the RFC to include the inherent methods discussed in "future possibilities". Then instead of EDIT: Done. |
I think wrapping should be used as the name instead of truncating, since technically converting for method naming, I like:
// all of these traits are designed to be extensible for things like
// BigInt or BigFloat or a 3rd party u256 or u5 or f16 implementation
pub trait WrapFrom<T> {
fn wrap_from(v: T) -> Self;
}
pub trait WrapTo<T> {
fn wrap_to(self) -> T;
}
impl<F, T> WrapTo<T> for F {..}
// maybe also have impl<T> WrapFrom<T> for T or impl<T: Into<U>, U> WrapFrom<T> for U
// SatFrom/SatTo/TruncSatFrom/TruncSatTo/LossyFrom/LossyTo just like above
impl all-primitive-ints-and-floats {
pub fn wrap_to<T>(self) where Self: WrapTo<T> {
WrapTo::wrap_to(self)
}
pub fn sat_to<T>(self) where Self: SatTo<T> {
SatTo::sat_to(self)
}
pub fn trunc_sat_to<T>(self) where Self: TruncSatTo<T> {
TruncSatTo::trunc_sat_to(self)
}
pub fn lossy_to<T>(self) where Self: LossyTo<T> {
LossyTo::lossy_to(self)
}
}
macro_rules! impl_from {
([$($from:ident),*] => $to:ident $Tr:ident::$f:ident($v:ident) $body:block) => {
$(impl $Tr<$from> for $to {
#[inline]
fn $f($v: $from) -> Self $body
})*
};
($from:tt => [$($to:ident),*] $Tr:ident::$f:ident($v:ident) $body:block) => {
$(impl_from!($from => $to $Tr::$f($v) $body);)*
};
}
impl_from! {
[u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize] =>
[u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize]
WrapFrom::wrap_from(v) {
v as _ // can be compiler magic instead of `as` in the future
}
}
impl_from! {
[u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize] =>
[u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize]
SatFrom::sat_from(v) {
if v < 0 {
if Self::MIN < 0 {
if v.wrap_to::<i128>() < Self::MIN.wrap_to::<i128>() {
Self::MIN
} else {
v.wrap_to()
}
} else {
0
}
} else if v.wrap_to::<u128>() > Self::MAX.wrap_to::<u128>() {
Self::MAX
} else {
v.wrap_to()
}
}
}
impl_from! {
[u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize] =>
[u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize]
TruncSatFrom::trunc_sat_from(v) {
v.sat_to()
}
}
impl_from! {
[f32, f64] =>
[u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize]
TruncSatFrom::trunc_sat_from(v) {
v as _
}
}
impl_from! {
[u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64] =>
[u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64]
LossyFrom::lossy_from(v) {
v as _
}
} |
@Aloso I share your concern about inference. let rf = (ru as f32) / (u8::MAX as f32); This is an entirely normal line of code you might see anywhere, but if these were turned into calls to |
Don't you mean the opposite? Generally the most significant bits are dropped. |
I meant to say least significant digit. (e.g. "4" in 0.1234) |
Oh, you were talking about floats. Understood |
I've deleted my previous comments because I was mistaken. TruncatingFrom is about integer types not floats. |
The |
I have tried my best to incorporate the feedback into the RFC. Any further feedback is of course welcome! |
I like word 42_u8.remainder::<i8>() == 42
-24_i8.remainder::<u8>() == 232 // u8::MAX + 1 - 24
232_u8.remainder::<i8>() == -24 // u8::MAX + 1 - 232
280_i16.remainder::<u8>() == 24 // 280 % u8::MAX = 24 <<-- here |
And 42_i8.nearest::<u8>() == 42
-14_i8.nearest::<u8>() == 0 // u8::MIN
169_u8.nearest::<i8>() == 127 // i8::MAX
280_i16.nearest::<u8>() == 255 // u8::MAX |
@VitWW the EDIT:
|
if the traits are implementable by 3rd party crates (so not sealed or perma-unstable), I strongly think there should be both |
The word "truncating" has an existing common meaning in the context of numeric conversions. This sense of the word truncate is also already present in Rust. It's confusing that this RFC is using the word "truncate" with a different meaning. The proposed |
@Aloso -24_i8.congruent::<u8>() == 232 // 2⁸ - 24
232_u8.congruent::<i8>() == -24 // 232 - 2⁸
536_i16.congruent::<u8>() == 24 // 536 mod 2⁸ |
this has some overlap with rust-lang/libs-team#204 |
|
agreed, as mentioned previously: #3415 (comment) |
I'm open to using the word "wrap" instead of "truncate" here, but I think the name fn foo(x: u8, y: i8) -> u8 {
x + y.wrap_to() // what?
} I'm not a fan of the name |
Honestly, would that even work? I think inference would just fail entirely since the int types have more than one Add impl, and so the compiler would get confused. |
yeah, it fails: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=25f46a7fabb838c24006cb166d2104fa |
That is unfortunate. I just checked that |
As someone who has developed What I would do is use
We can really go to town on bikeshedding since the additional I would additionally suggest not adding direct conversions between integers that are both of different sizes and signedness. The thing that personally weirded me about |
I strongly disagree.
on every platform it takes the input value and wraps to the output type. |
In the case of In the case of In the case of In the case of In the case of
The current |
I really prefer
|
This part I can't overcome unfortunately. I feel like |
an easy way to think about what happens is to think of all of e.g.: |
Previous alternative RFC discussion: Conversions: FromLossy and TryFromLossy traits #2484 |
|
||
However, I am convinced that removing (or at least reducing) this papercut will make Rust safer and prevent more bugs. This is similar in spirit to the `unsafe` keyword, which makes Rust more verbose, but also more explicit about potential problems. | ||
|
||
# Prior art |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Were there existing libraries doing something similar? I tried writing one myself, but I am the only user there. Would be great if there are some idiomatic libraries to sandbox this before it gets into the language.
This list of conversions should be implemented: | ||
|
||
- `TruncatingFrom` and `SaturatingFrom`: | ||
- all **signed** to **unsigned** integers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure it is a good idea to make u32 -> u16
and i16 -> u16
have the same trait? The implication feels quite different.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure at all. I'm open to introducing more granular traits and methods, if most people prefer it. But I find it difficult to judge the community's attitude towards this matter. Of course, people who are happy with the proposal in its current form are underrepresented in this discussion.
- `u32`, `u64`, `u128`, `i8`, `i16`, `i32`, `i64`, `i128` into `usize` | ||
- `u16`, `u32`, `u64`, `u128`, `i32`, `i64`, `i128` into `isize` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that u8
is not part of isize
because isize
is always a superset, but would it be more consistent to make them symmetric?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you suggesting to implement saturating and truncating methods for lossless conversions? I don't think that makes sense. Or what kind of symmetry are you looking for?
|
||
3. Instead of deprecating `as` only for lossy numeric casts, it could be deprecated for all numeric casts, so `From`/`Into` is required in these situations. | ||
|
||
This feels like overkill. If people really want to forbid `as` for lossless conversions, they can use clippy's `cast_lossless` lint. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any justification that people do not already use cast_lossless
? Or might it simply be because it is too troublesome to do .into()
and specify the proper type (because we don't have .into::<T>()
)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently, cast_lossless
isn't used because of the more verbose syntax. The rationale for this lint is: as
can sometimes perform lossy conversions, so using it even for lossless conversions is bad, because it may silently become lossy when changing types while refactoring.
Once lossy as
casts produce a warning, there will be no benefit to forbidding lossless as
casts.
|
||
4. The `approx()` method could have a more descriptive name. Likewise, `truncate()` isn't ideal since it sometimes wraps around. I am open to better suggestions (though bear in mind that having multiple methods, like `truncate()`, `wrap()` and `truncate_and_wrap()` will make the feature more complicated and harder to learn). | ||
|
||
5. `truncate` and `saturate` could be abbreviated as `trunc` and `sat`. This would make it more concise, which is appealing to people who convert between numbers a lot. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is already a trunc
method in f64
, not sure if that would cause confusion.
Add lossy numeric conversions as an alternative to the
as
operator, and deprecateas
for lossy numeric casts in a future edition, sobecomes
RENDERED
This solves the problem that when you see
as
, you don't know what it does. Does it truncate? saturate? round towards zero? lose numeric precision? Or is it lossless? Whenas
for lossy conversions is deprecated,as
is guaranteed to be lossless.