Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upConversions: `FromLossy` and `TryFromLossy` traits #2484
Conversation
dhardy
changed the title
New RFC: from-lossy
Conversions: `FromLossy` and `TryFromLossy` traits
Jun 22, 2018
Centril
added
the
T-libs
label
Jun 22, 2018
This comment has been minimized.
This comment has been minimized.
|
I think the word exact is problematic and that it defined imprecisely in this RFC. It suggests to me that for Since One possible definition that seems to fit the criteria described in the RFC is:
|
This comment has been minimized.
This comment has been minimized.
|
Other questions that this RFC provoked:
|
This comment has been minimized.
This comment has been minimized.
SoniEx2
commented
Jun 22, 2018
•
|
should From be allowed to allocate remote resources? (granted you should probably use TryFrom for things like that) |
This comment has been minimized.
This comment has been minimized.
I disagree with the need for an isomorphism; e.g. I see no problem with To be fair, that's not what you said. But surely an exact conversion
Meaning an intrinsic? I don't really see why this is important.
Since this wasn't specified before, I don't see it as a big issue, but I suppose it could be (if libs remove their implementations to comply).
It musn't fail (existing requirement, not from this RFC). Exactly what this means with regards to unlikely (and probably unrecoverable) panics is up for interpretation, I guess (and specifying something won't necessarily influence how people use it for anyway). |
This comment has been minimized.
This comment has been minimized.
|
@dhardy To be clear, I'm not saying that
That's exactly what the proposition says :) In other words:
or if we are inclined to be somewhat more pedantic:
No, "pure" means deterministic here. That is (formulated somewhat imprecisely), for I don't such a requirement is necessary tho? |
This comment has been minimized.
This comment has been minimized.
newpavlov
commented
Jun 22, 2018
|
It's not just about this RFC, but can't we make |
This comment has been minimized.
This comment has been minimized.
|
@Centril someone likes their type theory WP has an article on pure functions. But it's not the same as what you wrote which is about Back on topic, I guess all @newpavlov Good point. I'm not sure, is it possible to do this without it being a big breaking change? |
This comment has been minimized.
This comment has been minimized.
I do! ;) I think we should specify formally (with logic -- probably not typing rules) in the RFC and the documentation of Yeah being imprecise about what
If that is a reasonable expectation the user has, we might as well specify it? |
This comment has been minimized.
This comment has been minimized.
newpavlov
commented
Jun 22, 2018
•
|
@dhardy UPD: Hm, it looks automatic coercion is not planned, and it seems we'll get back to cyclic trait design from pub trait TryFrom<T>: Sized {
type Error;
fn from(value: T) -> Self {
Self::try_from(value).unwrap_or_else(|_| panic!("error"))
}
fn try_from(value: T) -> Result<Self, Self::Error> {
Ok(Self::from(value))
}
}
trait From = TryFrom<Error=!>;It will allow existing |
rkruppe
reviewed
Jun 22, 2018
|
I haven't formed an opinion on the actual proposal, so here's what I do best: nitpicking about floating point minutiae |
| This has several problems: | ||
|
|
||
| - `as` can perform several different types of conversion (as noted) and is therefore error-prone | ||
| - `as` can have [undefined behaviour](https://github.com/rust-lang/rust/issues/10184) |
This comment has been minimized.
This comment has been minimized.
rkruppe
Jun 22, 2018
Member
Nit: this is "just" an implementation bug, thus "temporary" and not a problem with as-the-language-feature.
This comment has been minimized.
This comment has been minimized.
dhardy
Jun 23, 2018
Author
Contributor
True, the undefined behaviour can be (and is being) fixed. I should say instead that there are certain casts which have no correct result and therefore should fail, which as has no mechanism for (other than panics).
|
|
||
| Where conversions are not exact, they should be equivalent to a conversion | ||
| which first forces some number of low bits of the integer representation to | ||
| zero, and then does an exact conversion to floating-point. |
This comment has been minimized.
This comment has been minimized.
rkruppe
Jun 22, 2018
Member
Is this trying to avoid specifying a rounding mode? Or is it trying to specify a rounding mode in some convoluted manner? Either way, all these conversions should be specified to round to nearest, ties to even. That's the default rounding mode and we (as well as other languages) use it everywhere except for float -> int conversions.
This comment has been minimized.
This comment has been minimized.
dhardy
Jun 23, 2018
Author
Contributor
True, this is convoluted. And you're correct, since the output type here is floating-point, it would be expected to round to nearest with ties to even.
|
|
||
| The implementations should fail on NaN, Inf, negative values (for unsigned | ||
| integer result types), and values whose integer approximation is not | ||
| representable. The integer approximation should be the value rounded towards |
This comment has been minimized.
This comment has been minimized.
rkruppe
Jun 22, 2018
Member
Failing if the integer approximation doesn't fit the target type is... debatable. The proposed semantics for as are to saturate to int_type::MAX or int_type::MIN respectively, and that is in some ways an "approximately equivalent value".
BTW the same point applies to infinities (i.e., it's reasonable to define f32::INFINITY -> u8 to result in 255), but I can understand being more uneasy with that.
This comment has been minimized.
This comment has been minimized.
dhardy
Jun 23, 2018
Author
Contributor
No, infinities and out-of-range values fall into the same category here: not representable. IMO 255 is a very poor approximation of 1000f32 and an even worse approximation to 1e300f64.
Certainly I'm being opinionated here but I think if we have a fallible conversion available then we should fail on out-of-range values. (But if not, then TryFromLossy also has no reason to exist.)
This comment has been minimized.
This comment has been minimized.
rkruppe
Jun 23, 2018
Member
I have sympathy with that argument, but at the same time I'd like to reduce divergence from the semantics of as (to avoid people sticking to as because they prefer its behavior), and currently that seems more likely to be saturation than panicking.
This comment has been minimized.
This comment has been minimized.
dhardy
Jun 23, 2018
Author
Contributor
Also note that as must return some defined value simply in order to avoid undefined behaviour. We have no need to model this conversion trait on the limitations of as.
This comment has been minimized.
This comment has been minimized.
rkruppe
Jun 23, 2018
Member
as doesn't have to return a value, it can panic (there are some reasons to not want that, but it avoids UB and is reasonable in isolation). That would be the moral equivalent of this trait returning Err.
| - 100_000f32 → u16: error | ||
|
|
||
| (Alternatively we could allow floats in the range (-1, 0] to convert to 0, also | ||
| for unsigned integers.) |
This comment has been minimized.
This comment has been minimized.
rkruppe
Jun 22, 2018
Member
That seems more consistent with the stated rounding mode (I view all these conversions as "round, then see if they fit into the target type") and it also matches the current behavior of as.
| negative, infinite or an NaN. So even though the output type has large enough | ||
| range, this conversion trait is still applicable.) | ||
|
|
||
| The implementations should fail on NaN, Inf, negative values (for unsigned |
This comment has been minimized.
This comment has been minimized.
rkruppe
Jun 22, 2018
Member
Does "negative values" include negative zero?
(This question disappears if values in (-1.0, 0.0] result in Ok(0) as discussed below.)
This comment has been minimized.
This comment has been minimized.
dhardy
Jun 23, 2018
Author
Contributor
No, not last time I checked
But this question goes away if we use the alternative as you suggested anyway.
| representable. The integer approximation should be the value rounded towards | ||
| zero. E.g.: | ||
|
|
||
| - 1.6f32 → u32: 2 |
This comment has been minimized.
This comment has been minimized.
dhardy
added some commits
Jun 23, 2018
This comment has been minimized.
This comment has been minimized.
|
Updated regarding several things mentioned. |
This comment has been minimized.
This comment has been minimized.
|
This comment has been minimized.
This comment has been minimized.
|
@repax The documentation of But yes, saying that The link to But regardless, it is nice to see a movement towards more exact definitions, kudos to @dhardy on that. |
This comment has been minimized.
This comment has been minimized.
|
@Centril of course all functions are homomorphisms w.r.t. equality (whoops). I guess we can drop the @SimonSapin thanks for the feedback.
I suppose there should be |
This comment has been minimized.
This comment has been minimized.
I’m having a hard time imagining a situation where generic code with these traits in a If I remember correctly the existence of both rust-lang/rust#42456 was another attempt that could be mentioned in Prior Art, even though it didn’t land. |
This comment has been minimized.
This comment has been minimized.
burdges
commented
Jun 23, 2018
•
|
Adding traits sounds unnecessarily confusing and hierarchical. And maybe breaking. Could
If you needed this, then you'd write |
This comment has been minimized.
This comment has been minimized.
|
@SimonSapin this is what motivated me. Ideally we wouldn't need to implement this trait ourselves. But it may not have wide usage. @burdges interesting idea. But why use |
This comment has been minimized.
This comment has been minimized.
|
@dhardy This example is sort of making my point. As far as I can tell it is not generic, and Is there a use case for code like either of these? fn foo<N>(…) where N: CastFromInt<u32> {…}
fn bar<N>(…) where u32: CastFromInt<N> {…} |
This comment has been minimized.
This comment has been minimized.
burdges
commented
Jun 24, 2018
•
|
I think |
This comment has been minimized.
This comment has been minimized.
|
Perhaps not. So I guess the alternative is simple but involves quite a few methods: add these to every integer type: fn to_f32(self) -> f32;
fn to_f64(self) -> f64;These types already have Or more akin to fn from_int<T: ToFloat<Self>>(x: T) -> Self;But @burdges |
This comment has been minimized.
This comment has been minimized.
burdges
commented
Jun 24, 2018
|
We do nee a default regardless. I suppose you're saying If you want to avoid breaking changes then you can specify it like this:
|
This comment has been minimized.
This comment has been minimized.
|
@burdges adding constants like that seems way more confusing (and also breaking) than what is proposed in the RFC. IMO it was a mistake to add Having more traits is fine, but I think it would be useful to reduce the number of traits which must be implemented, so I think going forward all new infallible traits should have a generic implementation for types implementing the infallible trait with an error type of impl<T, U> FromLossy<T> for U where U: TryFromLossy<T, Error=!> { ... }Also, @dhardy, the trait definitions in the RFC are missing their generic parameter |
This comment has been minimized.
This comment has been minimized.
burdges
commented
Jun 24, 2018
|
I imagined the coherence rules isolated any breakage when adding a default constant, but.. I suppose you're talking about how the trickier coherence rules around the existing type parameter, yes? I suggested constants here largely because they permit positive or indeterminate defaults, while traits mostly only support negative defaults. It's possible negative defaults are desirable here, but the RFC design looked kinda bent around negative defaults. I think folks have voiced roughly that opinion, hence my initial suggestion being a default As an aside, constants are less confusing because they're documented with the main trait, but rust doc could be modified to have subordinate traits documented on the same page as the main trait, so whatever. Oh. If one really wants unspecified defaults, one might also define |
This comment has been minimized.
This comment has been minimized.
|
@Diggsey what's your take on @SimonSapin's point that it may be better to find options which don't involve adding to the prelude? Auto-implementing |
This comment has been minimized.
This comment has been minimized.
|
It's a good point - I would at least like a solution which is generic over the numeric types (including user-defined numeric types) but I don't think it need be more general than that. Regarding the prelude - would it be possible to have the traits exist in both the prelude and a module, and only stabilise the version outside the prelude? (Effectively preventing the kinds of conflicts @SimonSapin described without committing to stabilisation of the trait's existence in the prelude). TBH, I'm not sure these need to be in the prelude when the |
This comment has been minimized.
This comment has been minimized.
|
IMHO, things like |
This comment has been minimized.
This comment has been minimized.
|
As far as I know impl deprecation isn’t really a thing. |
This comment has been minimized.
This comment has been minimized.
|
I really like I'm not a fan of trying to turn From/TryFrom into a single trait. I definitely don't want anything on TryFrom that reports failure by panicking; someone can (Given a time machine, I'd rather like I definitely agree with omitting Obviously I would be happy to see In fact, the phrasing as The question of rounding mode is an interesting one. I think the word "rounding" suggests to me that |
This comment has been minimized.
This comment has been minimized.
This is an important point. While I don't think that's something that can be taken for granted (since there are several ways to round things), it could be very misleading if in fact it rounds down (or to zero, or anything other than to nearest). So much as I like the name The other point about using the |
This comment has been minimized.
This comment has been minimized.
I meant the On round-to-nearest, what does |
This comment has been minimized.
This comment has been minimized.
How would we implement that? IIRC there were some undefined behavior related issues with changing the rounding mode in LLVM to do these types of computations. |
This comment has been minimized.
This comment has been minimized.
peterjoel
commented
Aug 5, 2018
|
Many existing Any An alternative to this RFC might be to acknowledge that
The definitions – |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
They can be lossy in the sense that the reverse mapping is not always unique, but they are usually to some extent "obvious". For example For numeric conversions there are multiple possible (and useful) behaviors for lossy cases. This is the reason why I think this RFC should not attempt full generality similar to |
dhardy
added some commits
Aug 13, 2018
This comment has been minimized.
This comment has been minimized.
|
I will push some minor tweaks to the RFC, but still not sure what to do with regards to rounding behaviour etc. For
Of the above, it seems that it may be worth supporting both saturating casts and explicit errors, e.g. via Then there is rounding behaviour (see @fstirlitz's post):
Of the above, it would be nice to support at least the first two options; maybe also round up/down. However, how should this be done? One possibility would be to have multiple methods within a trait TryRoundFrom<T> {
type Error;
fn try_trunc_from(x: T) -> Result<Self, Self::Error>;
fn try_round_from(x: T) -> Result<Self, Self::Error>;
} |
This comment has been minimized.
This comment has been minimized.
Doesn't |
This comment has been minimized.
This comment has been minimized.
|
What does truncation mean for values out of range of the target type? Also, what's a good way to test these instructions? Writing ASM I suppose... |
This comment has been minimized.
This comment has been minimized.
|
Julia has a nice approach to conversions, basically parameterised rounding functions. Unfortunately by the time you add error handling and account for Rust not allowing default values for type arguments, the nearest equivalent is much less ergonomic): trait Round<T> {
type Error;
fn round(&self) -> Result<T, Error>;
}
let y = Round::<u32>::round(2.6f32)?;
// In Julia, it always rounds to the nearest integer:
assert_eq!(Round::<f64>::round(10.1).unwrap(), 10.0);
// though we'd probably not want to do thisThe above is not exactly ergonomic Rust. But regarding @SimonSapin's previous point about focusing on numeric conversions, it suggests the following direction:
Which brings up another point: why are the error types Or, there is another possible approach, though more convoluted and probably a bad idea: /// pure marker wrapper
pub struct Round<T>(T);
impl TryFrom<Round<f32>> for u32 { ... } |
This comment has been minimized.
This comment has been minimized.
|
Catching up with the last three(!) weeks of discussion, here's assorted notes about implementability and performance (sorry still don't have an opinion on the bikesheds):
|
This comment has been minimized.
This comment has been minimized.
|
Thanks @rkruppe! For saturating conversions (clamping to target range), is it reasonable to only support a single rounding mode (perhaps truncation) or is there reason to support multiple rounding modes, as with other conversions? Also, for saturating conversions from floating point, what should be the behaviour on NaN and Inf input? (Do we need error handling anyway, via Because saturating conversions could be as complex as non-saturating (erroring) variants, or could be simplified to a single trait & function. @SimonSapin would you know if there are any proposals similar to Or will we even get free unwrapping from The point is that in this case we can use uniform error handling ( |
This comment has been minimized.
This comment has been minimized.
|
@dhardy Per http://smallcultfollowing.com/babysteps/blog/2018/08/13/never-patterns-exhaustive-matching-and-uninhabited-types-oh-my/ it sounds like the plan is to allow this: fn foo(x: Result<T, !>) {
let Ok(value) = x; // This pattern is irrefutable, since the Err(!) case is impossible
}… but I don’t know of any proposal for doing it within an expression in any way close in terseness to the |
This comment has been minimized.
This comment has been minimized.
I don't see why the handling out values outside the target range should be related to how you're rounding fractional quantities within the range. |
This comment has been minimized.
This comment has been minimized.
It's not related; it's just an attempt to simplify. Because so far it looks like we might want all these: fn trunc_from(x: T) -> Result<Self, Error>;
fn round_from(x: T) -> Result<Self, Error>;
fn ceil_from(x: T) -> Result<Self, Error>;
fn floor_from(x: T) -> Result<Self, Error>;
fn saturating_trunc_from(x: T) -> Result<Self, Error>;
fn saturating_round_from(x: T) -> Result<Self, Error>;
fn saturating_ceil_from(x: T) -> Result<Self, Error>;
fn saturating_floor_from(x: T) -> Result<Self, Error>;(and this is assuming we don't have separate Of course, we could reduce this list by means of a Or we could use |
This comment has been minimized.
This comment has been minimized.
vks
commented
Aug 30, 2018
Alternatively, there could be an |
This comment has been minimized.
This comment has been minimized.
leonardo-m
commented
Aug 30, 2018
|
Currently "as" casts are a language feature, and they are a basic feature in a language, so I think the problem of casts should be discussed and worked on from a more general point of view, instead of just from a stdlib point of view like (I think) it's being discussed here, to face the bigger problem. I think the first we should take a look at Rust code to see where most casts are, to classify them and see how much dangerous they could be. In past I've written various posts about this, like: https://internals.rust-lang.org/t/to-reduce-the-number-of-true-casts/3076 https://users.rust-lang.org/t/correctness-integral-overflows/6449/ In many cases the casts are like:
So a well rounded proposal for casts should face the problem of array/slice/vec indexes. Another common situation is this one, Rust should offer a safe cast (that doesn't contain an unwrap) to do this:
To do this I think a stdlib-level solution isn't good enough, you need to bake inside the compiler some value-range analysis. Also, number casts aren't the only thing we should care about. There are also enums: And slices, currently in Nightly you can do this:
But the slice <-> array conversions is currently too much underpowered, tricky and unergonomic, it should become more flexible and simpler to use, keeping code correctness. Beside the value-range analysis of numeric expressions the compiler could perform analysis on the length of the slices too, to allow slice <-> array conversions with a light syntax, keeping safety and avoiding unwraps() where the slice->array conversion is provably always correct (like in the |
This comment has been minimized.
This comment has been minimized.
mgeisler
commented
Sep 1, 2018
•
|
I noticed that the RFC and the discussion above has been almost exclusively focussed on numeric conversions. However, it is my impression that Similarly, while rounding errors is one good example of a lossy conversion, I would also expect Going further, one could imagine an image manipulation library using |
This comment has been minimized.
This comment has been minimized.
|
From this example: let i: u16 = ...;
my_slice[i as usize] = 0;one would think it could be solved by by implementing In C++ one could take a different approach: implicit conversions. Rust doesn't have them and C++ over-uses them, but perhaps this would be a legitimate use (along with some other conversions which are simple and loss-less). |
This comment has been minimized.
This comment has been minimized.
vks
commented
Sep 1, 2018
I don't think this this is feasible, because it breaks type inference, likely for a lot of code. |
This comment has been minimized.
This comment has been minimized.
|
True, that wouldn't work because of type inference. What might work is (a) But annoyingly literals need special attention to avoid breakage. |
dhardy commentedJun 22, 2018
Add
FromLossy,TryFromLossytraits.Discuss the bigger picture of conversions and the
askeyword.Specify that
Fromimplementations must not only be safe, but also exact.Rendered RFC