From 53d388fb96d28a0af074df17cc510895f690e435 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Fri, 22 Jun 2018 15:39:08 +0100 Subject: [PATCH 1/9] New RFC: from-lossy --- text/0000-from-lossy.md | 321 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 text/0000-from-lossy.md diff --git a/text/0000-from-lossy.md b/text/0000-from-lossy.md new file mode 100644 index 00000000000..641776e2fa1 --- /dev/null +++ b/text/0000-from-lossy.md @@ -0,0 +1,321 @@ +- Feature Name: from-lossy +- Start Date: 2018-06-22 +- RFC PR: (leave this empty) +- Rust Issue: (leave this empty) + +# Summary +[summary]: #summary + +Add `FromLossy`, `TryFromLossy` traits. + +Discuss the bigger picture of conversions and the `as` keyword. + +Specify that `From` implementations must not only be *safe*, but also *exact*. + +# Motivation +[motivation]: #motivation + +Currently many numeric conversions can only be done via the `as` keyword or third-party libraries. To [quote @scottmcm](https://internals.rust-lang.org/t/lossy-conversion-trait-as/7672/4?u=dhardy): + +> I’m strongly against [a trait] that’s just “whatever as does”, since I find it an unfortunate mess of conversions right now – some coercions, some extensions, some truncations, … + +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) +- since `as` is not a trait, it cannot be used as a bound in generic code (excepting via `num_traits::cast::AsPrimitive`) +- these conversions are mostly primitive instructions and are very widely used, so requiring users to use another crate like `num` is unexpected + +Several types of conversion are possible: + +- safe, exact conversions (e.g. `u32` → `u64`) are handled by the `From` trait ([RFC 529](https://github.com/rust-lang/rfcs/pull/529)) +- fallible conversions (e.g. `u32` → `i8`) are handled by the `TryFrom` trait ([RFC 1542](https://github.com/rust-lang/rfcs/pull/1542)) +- lossy conversions (e.g. `i64` → `f32`) +- lossy fallible conversions (e.g. `f64` → `u64`) +- truncations on unsigned integers (e.g. `u64` → `u32`, dropping unused high bits) +- sign-ignoring coercions/transmutations (e.g. `i8` → `u8`, `i64` → `i32`); + these can yield totally different values due to interpretation of the sign + bit (e.g. `3i32 << 14` is 49152=2^14+2^15; converting to `i16` yields -16384=2^14-2^15) +- conversions between types with platform-dependent size (i.e. `usize` and `isize`) + +This RFC is principally concerned with lossy conversions, but considers other +conversions for a broader picture. + +It is *my opinion* that we should cover all *common* conversions performed by +the `as` keyword redundant with `std` library functionality, and *consider* +deprecating `as` eventually. Whether this entails traits covering all the above +conversions or methods on primitive types for some conversions, or even leaves +no alternative to `transmute` in some cases is beyond the scope of this RFC. + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +Type conversions can be handled by the following traits: + +- `From` for infallible, exact conversions (e.g. widening, `u8` → `u16`) +- `TryFrom` for fallible, exact conversions (e.g. narrowing, `u16` → `u8`, and signed, `i16` → `u16`) +- `FromLossy` for infallible, lossy conversions (mostly concerning floating-point types, e.g. `u32` → `f32`) +- `TryFromLossy` for fallible, inexact conversions (e.g. `f32` → `u32`) +- `TruncateFrom` for truncations (e.g. `u16` → `u8` which drops high bits) + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +### `From` trait + +Tweak the documentation to clarify this. + +Remove several implementations on SIMD types (currently including many fallible +conversions like `f32x4` → `i8x4`; nightly only). + +### `FromLossy` trait + +Add `std::convert::FromLossy`: + +```rust +/// A trait for conversions which are potentially inexact. +/// +/// It is required that conversions do not fail, and required that results are +/// either approximately equivalent or are an appropriate special value. +/// For example, it is reasonable for an implementation converting `f64` to +/// `f32` to lose precision and to return positive or negative infinity for +/// out-of-range results, as well as to drop payloads from NaN values. +pub trait FromLossy { + fn from_lossy(x: T) -> Self; +} +``` + +Add implementations to `f64` from all of: + +- `u64, i64, u128, i128` + +and to `f32` from all of: + +- `f64, u32, i32, u64, i64, u128, i128` + +(Note: other integer → float conversions are +already handled by `From`. There is a question below about trait overlap.) + +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. + +### `TryFromLossy` trait + +Add `std::convert::TryFromLossy`: + +```rust +/// A trait for conversions which may fail and may be inexact. +/// +/// Implementations should fail when the output type has no suitable +/// corresponding or general-purpose value, and return an approximately +/// equivalent value when possible. +pub trait TryFromLossy { + type Error; + fn try_from_lossy(x: T) -> Result; +} +``` + +Add implementations from all of: + +- `f32, f64` + +to all of: + +- `u8, u16, u32, u64, u128` +- `i8, i16, i32, i64, i128` + +(Note: `f32` → `u128` is infallible (but still lossy) *if* the input is not +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 +integer result types), and values whose integer approximation is not +representable. The integer approximation should be the value rounded towards +zero. E.g.: + +- 1.6f32 → u32: 2 +- -0.2f32 → u32: error +- -0.6f32 → i32: 0 +- 100_000f32 → u16: error + +(Alternatively we could allow floats in the range (-1, 0] to convert to 0, also +for unsigned integers.) + +# Related problems + +These problems are discussed in the search for a complete solution; however it +is not currently proposed to solve them within this RFC. + +## Integer transmutations + +There are several types of transmutation, discussed here separately, although +they are all transmutations. + +Note that there is less insentive against usage of `as` in these cases since the +conversions do not preserve "value", although alternatives still have some use +(e.g. to clarify that a conversion is a truncation). + +### Sign transmutations + +The conversions done by `as` between signed and unsigned types of the same size +are simply transmutations (reinterpretations of the underlying bits), e.g. +`0x80u8 as i8 == -128`. + +As these are not simple mathematical operations we could simply not provide any +alternative to `as`, and suggest usage of `mem::transmute` if necessary. + +Alternatively, we could add `transmute_sign` methods to all primitive integer +types, e.g.: +```rust +impl i32 { + ... + fn transmute_sign(self) -> u32 { ... } +} +``` + +### Unsigned truncation + +We could add `std::convert::TruncateFrom`: + +```rust +/// A trait for conversions which are truncate values by dropping unused high +/// bits. +/// +/// Note that this is distinct from other types of conversion since high bits +/// are explicitly ignored and results are thus not numerically equivalent to +/// input values. +pub trait TruncateFrom { + fn truncate_from(x: T) -> Self; +} +``` + +Add implementations for each unsigned integer type to each smaller unsigned +integer type. (See below regarding signed types.) + +Note that we *could* suggest users drop unwanted high bits (via masks or +bit-shifting) *then* use `TryFrom`, but this is a very unergonomic approach to +what is a simple and commonly used operation. + +### Signed truncation + +Bitwise operations on signed integers can have "unintuitive" results. For example, +```rust +fn main() { + let x = 3i32 << 14; + println!("{}", x); + println!("{}", x as i16); +} +``` +prints: +``` +49152 +-16384 +``` +since the 16th bit is later interpreted as a -215 in the Two's +Complement representation, the numeric value on conversion to `i16` is quite +different despite all the dropped bits being 0. + +Essentially, operations like `i32` → `i16` are *shorten-and-transmute*. + +Since these operations are not intuitive and not so widely useful, it may not +be necessary to implement traits over them. + +Instead, we could suggest users implement signed truncations like this: +`x.transmute_sign().truncate_into::().transmute_sign()`. + +## Platform-dependent types + +### isize / usize + +The `isize` and `usize` types have undefined size, though there appears to be +an assumption that they are at least 16 bits (existing `From` implementations) +and that they could be larger than 64 bits. +[Discussion thread on internals](https://internals.rust-lang.org/t/numeric-into-should-not-require-everyone-to-support-16-bit-and-128-bit-usize/3609/7). +[Discussion about `usize` → `u64` on users forum](https://users.rust-lang.org/t/cant-convert-usize-to-u64/6243). + +# Drawbacks +[drawbacks]: #drawbacks + +This RFC proposes at least two new conversion traits and *still* doesn't solve +all conversion problems (notably, platform-depedent types). + +It also makes numeric conversions complex, and *deliberately does so*. This does +of course make the language more difficult to learn. Is there a simpler option? +The current undefined behaviour when converting from floating-point types +highlights the problems with a very generic solution. + +# Rationale and alternatives +[alternatives]: #alternatives + +As mentioned, the `as` keyword has multiple problems, is not Rustic, and we +should aim to provide users with *safer* alternatives. + +This RFC mostly approaches the problem via conversion traits, though as +highlighted with the suggested `transmute_sign` method, traits are not the only +approach. Since multiple types of conversion have multiple possible target types +for each initial type, traits are however appropriate. + +There is also the possibility to leave this type of conversion trait to external +libraries. Given how widely useful some of these conversions are (especially +those to/from floating-point types), it would be desireable to have a `std`-lib +solution. + +As a simplification, all integer transmutation conversions discussed above could +be handled by a single trait, as a "safe" alternative to `mem::transmute`. + +# Prior art +[prior-art]: #prior-art + +Rust-specific: + +- Conversion traits: [RFC 529](https://github.com/rust-lang/rfcs/pull/529) +- `TryFrom` trait: [RFC 1542](https://github.com/rust-lang/rfcs/pull/1542) +- internals thread: https://internals.rust-lang.org/t/lossy-conversion-trait-as/7672/4 +- internals thread: https://internals.rust-lang.org/t/numeric-into-should-not-require-everyone-to-support-16-bit-and-128-bit-usize/3609/7 +- users thread: https://users.rust-lang.org/t/cant-convert-usize-to-u64/6243 +- [`num_traits::cast::cast`](https://docs.rs/num-traits/0.2.4/num_traits/cast/fn.cast.html) + +C++ tries to add some degree of explicitness with [`static_cast`](https://en.cppreference.com/w/cpp/language/static_cast) etc., but with much less granularity than discussed here. + +# Unresolved questions +[unresolved]: #unresolved-questions + +**Should we add `FromLossy` implementations from `usize`/`isize` types?** + +I suspect it is better to leave this decision until another RFC solves +conversions on those types. + +**Should we allow overlapping conversion implementations?** + +E.g. `FromLossy` could have a blanket implementation for all `From` +implementations, and similarly for `TryFromLossy` and `TryFrom`. +Similarly, `TryFrom` could be implemented for all `From` implementations, +and the same for `TryFromLossy` for `FromLossy` implementations — except this +would have rules to implement `TryFromLossy` for `From` implementations through +both `FromLossy` and `TryFrom`, which is a type error (possibly solvable with +specialization). + +This would be useful in generic code, but isn't strictly necessary. +Unfortunately without these overlapping implementations generic code wanting to +support types over multiple conversion traits requires a local re-implementation; +worse, this is a technique used today, thus adding overlap today will cause +breakage (although again specialization may solve this). + +**Should the new traits be added to the prelude?** + +If the ultimate aim is to make the `as` keyword redundant, then probably the answer is yes, but I will refer you to [this post by @alexcrichton](https://github.com/rust-lang/rfcs/pull/1542#issuecomment-211592332): + +> On question I'd have is whether we'd want to add these traits to the prelude? One of the reasons Into and From are so easy to use is because of their prominent placement and lack of a need to import them. I'd be of the opinion that these likely want to go into the prelude at some point, but we may not have all of the resolve changes in place to do that without unduly breaking crates. To me though one of the major motivations for this RFC is integer conversions, which I believe aren't gonna work out unless they're in the prelude, so I'd be fine adding it sooner rather than later if we can. + +**Should we eventually remove the `as` keyword?** + +I suspect it is too late to make such a change for Rust 2018, and do not see a +need to rush this (at the very least, conversions involving `usize` and `isize` +also need solving before removal of `as`). + +Removing this keyword would no doubt be contraversial and would further distance +Rust from *convenient* languages like Python, so this is not a decision to make +lightly. + +Alternatively we could simply rely on Clippy to lint against "unnecessary" uses of `as`. From 8ba9ec2e41cc40ef78deea1de1c6a83b57135229 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 23 Jun 2018 14:11:06 +0100 Subject: [PATCH 2/9] Revise from-lossy regrading @rkruppe's review --- text/0000-from-lossy.md | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/text/0000-from-lossy.md b/text/0000-from-lossy.md index 641776e2fa1..d27d6708808 100644 --- a/text/0000-from-lossy.md +++ b/text/0000-from-lossy.md @@ -22,7 +22,9 @@ Currently many numeric conversions can only be done via the `as` keyword or thir 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) +- `as` conversions must be infallible, which allows no correct result for some + operations, for example when converting very large floats to integers (see + [this issue](https://github.com/rust-lang/rust/issues/10184)) - since `as` is not a trait, it cannot be used as a bound in generic code (excepting via `num_traits::cast::AsPrimitive`) - these conversions are mostly primitive instructions and are very widely used, so requiring users to use another crate like `num` is unexpected @@ -93,12 +95,11 @@ and to `f32` from all of: - `f64, u32, i32, u64, i64, u128, i128` -(Note: other integer → float conversions are -already handled by `From`. There is a question below about trait overlap.) +(Note: other integer → float conversions are already handled by `From` since +they are loss-less. There is a question below about trait overlap.) -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. +These conversions should round to the nearest representable value, with ties to +even (as is commonly used for floating-point operations). ### `TryFromLossy` trait @@ -129,19 +130,15 @@ to all of: 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 -integer result types), and values whose integer approximation is not -representable. The integer approximation should be the value rounded towards -zero. E.g.: +The implementations should fail on NaN, Inf, and values whose integer +approximation is not representable. The integer approximation should be the +value rounded towards zero. E.g.: -- 1.6f32 → u32: 2 -- -0.2f32 → u32: error -- -0.6f32 → i32: 0 +- 1.6f32 → u32: 1 +- -1f32 → u32: error +- -0.2f32 → u32: 0 - 100_000f32 → u16: error -(Alternatively we could allow floats in the range (-1, 0] to convert to 0, also -for unsigned integers.) - # Related problems These problems are discussed in the search for a complete solution; however it From f7afd54d085b90de326eb00bcceac17c57f2f4b7 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 23 Jun 2018 15:33:01 +0100 Subject: [PATCH 3/9] Revise from-lossy regarding From exactness, genericity and isize/usize --- text/0000-from-lossy.md | 50 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/text/0000-from-lossy.md b/text/0000-from-lossy.md index d27d6708808..d11b5a37b0f 100644 --- a/text/0000-from-lossy.md +++ b/text/0000-from-lossy.md @@ -10,7 +10,8 @@ Add `FromLossy`, `TryFromLossy` traits. Discuss the bigger picture of conversions and the `as` keyword. -Specify that `From` implementations must not only be *safe*, but also *exact*. +Specify that `From` implementations must not only be *safe*, but also *exact* +(homomorphisms). # Motivation [motivation]: #motivation @@ -65,10 +66,32 @@ Type conversions can be handled by the following traits: ### `From` trait -Tweak the documentation to clarify this. +Tweak the documentation to clarify that `From` implementations should be +monomorphisms with regards to the `==` operation: -Remove several implementations on SIMD types (currently including many fallible -conversions like `f32x4` → `i8x4`; nightly only). +```rust +/// Simple and safe type conversions in to Self. It is the reciprocal of Into. +/// +/// Implementations are expected to be injective homomorphisms with respect to +/// the `PartialEq` implementations of source and target types, as well as the +/// inverse of `PartialEq`: that is, with target type `T` and any `x, y` of +/// source type `S`, then `x == y` if and only if `T::from(x) == T::from(y)`. +.. +pub trait From { +``` + +Note that monomorphisms are not isomorphisms since it is not required that the +functions have injective inverse (i.e. we may define `From for u16` and an +inverse `g` such that `g(u16::from(x)) == x` for `x: u8`, but it does not hold +that `u16::from(g(y)) == y` for `y: u16`). + +Note also that for the purposes of this requirement we consider the `PartialEq` +implementation (if available) and not binary value; for example we do not +require that `0f32` and `-0f32` map to different binary values. + +Nightly rust currently has several implementations of `From` on SIMD types +which should be removed (e.g. `f32x4` → `i8x4` (fallible) and `u64x4` → `f32x4` +(lossy i.e. not injective)). ### `FromLossy` trait @@ -231,6 +254,12 @@ and that they could be larger than 64 bits. [Discussion thread on internals](https://internals.rust-lang.org/t/numeric-into-should-not-require-everyone-to-support-16-bit-and-128-bit-usize/3609/7). [Discussion about `usize` → `u64` on users forum](https://users.rust-lang.org/t/cant-convert-usize-to-u64/6243). +Checked integer conversions using `TryFrom` [are being reintroduced](https://github.com/rust-lang/rust/issues/49415). + +It is possible that unchecked conversions could be added, perhaps using +`TruncateFrom` (or some other trait allowing both truncation and +zero-extension). + # Drawbacks [drawbacks]: #drawbacks @@ -283,6 +312,19 @@ C++ tries to add some degree of explicitness with [`static_cast`](https://en.cpp I suspect it is better to leave this decision until another RFC solves conversions on those types. +**Should the infallible traits be special cases of the fallible traits?** + +@newpavlov points out that `From` is equivalent to `TryFrom` +and similarly for `FromLossy` and `TryFromLossy`. Should the former traits +therefore just be aliases of the latter? + +Since `From` is already stable we cannot tweak it in backward incompatible ways, +so the first question should be whether this change can be made without +breakage. + +Probably it will be better to revisit this later (but there are implications on +the next question). + **Should we allow overlapping conversion implementations?** E.g. `FromLossy` could have a blanket implementation for all `From` From fd94ccef89a5c625756c2bf27776926d292e9e66 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 30 Jun 2018 12:03:52 +0100 Subject: [PATCH 4/9] Revise from-lossy RFC: more precise definitions of lossy and approximate conversion --- text/0000-from-lossy.md | 78 ++++++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 28 deletions(-) diff --git a/text/0000-from-lossy.md b/text/0000-from-lossy.md index d11b5a37b0f..a7407803b01 100644 --- a/text/0000-from-lossy.md +++ b/text/0000-from-lossy.md @@ -53,6 +53,10 @@ no alternative to `transmute` in some cases is beyond the scope of this RFC. # Guide-level explanation [guide-level-explanation]: #guide-level-explanation +We use informal definitions here using the word *lossy*, implying that some +precision may be lost. *All conversions should preserve value at least +approximately* (excepting `TruncateFrom` which has a different interpretation). + Type conversions can be handled by the following traits: - `From` for infallible, exact conversions (e.g. widening, `u8` → `u16`) @@ -66,29 +70,28 @@ Type conversions can be handled by the following traits: ### `From` trait -Tweak the documentation to clarify that `From` implementations should be -monomorphisms with regards to the `==` operation: +Tweak the documentation to clarify that `From` implementations should not lose +precision (i.e. should be injective): ```rust -/// Simple and safe type conversions in to Self. It is the reciprocal of Into. +/// Simple and safe type conversions to `Self`. It is the reciprocal of `Into`. +/// +/// Implementations should not lose precision. That is, given `from` mapping +/// from type `S` to type `T` (`from: S ↦ T`), it should be possible to define +/// an inverse mapping `g: T ↦ S` such that for all `x ∈ S`, `g(from(x))` is +/// equivalent to `x`. This implies that `from` must be injective. Note that +/// function `g` may be extended to a `TryFrom for S` implementation, +/// though this is not required. /// -/// Implementations are expected to be injective homomorphisms with respect to -/// the `PartialEq` implementations of source and target types, as well as the -/// inverse of `PartialEq`: that is, with target type `T` and any `x, y` of -/// source type `S`, then `x == y` if and only if `T::from(x) == T::from(y)`. +/// Where `S: Eq`, we use `Eq` as our equivalence relation; otherwise, where +/// `S: PartialEq`, we form an equivalence relation consistent with `PartialEq` +/// over the subset of elements where `PartialEq` is reflexive (i.e. for `x` +/// where `x.eq(x)`), and consider all other elements to be in an "other" +/// subset `O` such that for `x, y ∈ O, x = y` and `x ∈ O, y ∉ O, x ≠ y`. .. pub trait From { ``` -Note that monomorphisms are not isomorphisms since it is not required that the -functions have injective inverse (i.e. we may define `From for u16` and an -inverse `g` such that `g(u16::from(x)) == x` for `x: u8`, but it does not hold -that `u16::from(g(y)) == y` for `y: u16`). - -Note also that for the purposes of this requirement we consider the `PartialEq` -implementation (if available) and not binary value; for example we do not -require that `0f32` and `-0f32` map to different binary values. - Nightly rust currently has several implementations of `From` on SIMD types which should be removed (e.g. `f32x4` → `i8x4` (fallible) and `u64x4` → `f32x4` (lossy i.e. not injective)). @@ -98,13 +101,28 @@ which should be removed (e.g. `f32x4` → `i8x4` (fallible) and `u64x4` → `f32 Add `std::convert::FromLossy`: ```rust -/// A trait for conversions which are potentially inexact. +/// A trait for conversions which may lose precision. /// -/// It is required that conversions do not fail, and required that results are -/// either approximately equivalent or are an appropriate special value. -/// For example, it is reasonable for an implementation converting `f64` to -/// `f32` to lose precision and to return positive or negative infinity for +/// Like `From`, implementations should not fail. +/// +/// Unlike `From`, implementations may lose precision, however, all results +/// should be approximations to the input value or are an appropriate special +/// value. For example, it is reasonable for an implementation converting `f64` +/// to `f32` to lose precision and to return positive or negative infinity for /// out-of-range results, as well as to drop payloads from NaN values. +/// +/// We do not specify the rounding mode used by implementations, but all results +/// which are not special values should be numerically *close to* the input +/// value. If `x` is the input value and `u` is the precision of the result type +/// at `x`, then it should normally hold that `|x - from_lossy(x)| < u`. The +/// precision `u` may be constant (as in integer types) or variable (as in +/// floating point types) but should be signficantly smaller than the magnitude +/// of x (`u << |x|`) and should be consistent with typical values for the +/// result type. +/// +/// If the mapping has no suitable approximate value for some inputs and no +/// special value which may be used instead, then conversion should be +/// implemented using the `TryFromLossy` trait instead. pub trait FromLossy { fn from_lossy(x: T) -> Self; } @@ -118,22 +136,26 @@ and to `f32` from all of: - `f64, u32, i32, u64, i64, u128, i128` -(Note: other integer → float conversions are already handled by `From` since -they are loss-less. There is a question below about trait overlap.) - These conversions should round to the nearest representable value, with ties to even (as is commonly used for floating-point operations). +(Note: other integer → float conversions are already handled by `From` since +they are loss-less. There is a question below about trait overlap.) + ### `TryFromLossy` trait Add `std::convert::TryFromLossy`: ```rust -/// A trait for conversions which may fail and may be inexact. +/// A trait for conversions which may fail and may lose precision. +/// +/// Implementations should fail when the result type has no reasonable +/// approximation of the input type and no appropriate special value (such as a +/// representation of overflown or non-numeric values); otherwise, the +/// conversion should succeed with an approximation of the input value. /// -/// Implementations should fail when the output type has no suitable -/// corresponding or general-purpose value, and return an approximately -/// equivalent value when possible. +/// For more precise definitions of "reasonable approximation" see the +/// documentation on the `FromLossy` trait. pub trait TryFromLossy { type Error; fn try_from_lossy(x: T) -> Result; From 1fec4e7cc0d8387c17540bbfabdd17f9aa64ebf2 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 30 Jun 2018 12:49:16 +0100 Subject: [PATCH 5/9] Revise from-lossy RFC: WrappingFrom and TryFrom doc & default impl --- text/0000-from-lossy.md | 186 ++++++++++++++++++++++++---------------- 1 file changed, 110 insertions(+), 76 deletions(-) diff --git a/text/0000-from-lossy.md b/text/0000-from-lossy.md index a7407803b01..d879a18752f 100644 --- a/text/0000-from-lossy.md +++ b/text/0000-from-lossy.md @@ -6,7 +6,7 @@ # Summary [summary]: #summary -Add `FromLossy`, `TryFromLossy` traits. +Add `FromLossy`, `TryFromLossy` and `WrappingFrom` traits. Discuss the bigger picture of conversions and the `as` keyword. @@ -55,7 +55,7 @@ no alternative to `transmute` in some cases is beyond the scope of this RFC. We use informal definitions here using the word *lossy*, implying that some precision may be lost. *All conversions should preserve value at least -approximately* (excepting `TruncateFrom` which has a different interpretation). +approximately* (excepting `WrappingFrom` which has a different interpretation). Type conversions can be handled by the following traits: @@ -63,11 +63,13 @@ Type conversions can be handled by the following traits: - `TryFrom` for fallible, exact conversions (e.g. narrowing, `u16` → `u8`, and signed, `i16` → `u16`) - `FromLossy` for infallible, lossy conversions (mostly concerning floating-point types, e.g. `u32` → `f32`) - `TryFromLossy` for fallible, inexact conversions (e.g. `f32` → `u32`) -- `TruncateFrom` for truncations (e.g. `u16` → `u8` which drops high bits) +- `WrappingFrom` for truncations (e.g. `u16` → `u8`) and sign coercions (e.g. `u16 → i16`) # Reference-level explanation [reference-level-explanation]: #reference-level-explanation +## (Approximate) semantic value preserving conversions + ### `From` trait Tweak the documentation to clarify that `From` implementations should not lose @@ -89,13 +91,61 @@ precision (i.e. should be injective): /// where `x.eq(x)`), and consider all other elements to be in an "other" /// subset `O` such that for `x, y ∈ O, x = y` and `x ∈ O, y ∉ O, x ≠ y`. .. -pub trait From { +pub trait From { + fn from(S) -> Self; +} ``` Nightly rust currently has several implementations of `From` on SIMD types which should be removed (e.g. `f32x4` → `i8x4` (fallible) and `u64x4` → `f32x4` (lossy i.e. not injective)). +### `TryFrom` trait + +This trait currently lacks documentation. It should have similar requirements +to the `From` trait: + +```rust +/// Potentially fallible type conversions to `Self`. It is the reciprocal of +/// `TryInto`. +/// +/// Currently `From` is a special case of `TryFrom`. Where conversions never +/// fail, `From` should be used instead. +/// +/// Successful conversions should not lose precision. That is, given `try_from` +/// mapping from type `S` to type `T` (`try_from: S ↦ T`), it should be possible +/// to define an inverse mapping `g: T ↦ S` such that for all `x ∈ S`, if +/// `try_from(x)` does not fail, then `g(try_from(x))` is equivalent to `x`. +/// This implies that `try_from` must be injective on the subset of the source +/// type `S` where conversions succeed. Note that function `g` may be extended +/// to a `TryFrom for S` implementation, though this is not required. +/// +/// Where `S: Eq`, we use `Eq` as our equivalence relation; otherwise, where +/// `S: PartialEq`, we form an equivalence relation consistent with `PartialEq` +/// over the subset of elements where `PartialEq` is reflexive (i.e. for `x` +/// where `x.eq(x)`), and consider all other elements to be in an "other" +/// subset `O` such that for `x, y ∈ O, x = y` and `x ∈ O, y ∉ O, x ≠ y`. +.. +pub trait TryFrom { + type Error; + fn try_from(value: S) -> Result; +} +``` + +It would be nice to be able to make `TryFrom` an extension of `From`. +Unfortunately this would be a potentially breaking change since more +concrete implementations of both traits may exist for some types; we therefore +propose to add a default implementation once +[specialisation](https://github.com/rust-lang/rfcs/pull/1210) is ready: +```rust +impl TryFrom for T where From: T { + default type Error = !; + default fn try_from(value: S) -> Result { + Ok(T::from(value)) + } +} +``` + ### `FromLossy` trait Add `std::convert::FromLossy`: @@ -123,8 +173,8 @@ Add `std::convert::FromLossy`: /// If the mapping has no suitable approximate value for some inputs and no /// special value which may be used instead, then conversion should be /// implemented using the `TryFromLossy` trait instead. -pub trait FromLossy { - fn from_lossy(x: T) -> Self; +pub trait FromLossy { + fn from_lossy(x: S) -> Self; } ``` @@ -149,16 +199,37 @@ Add `std::convert::TryFromLossy`: ```rust /// A trait for conversions which may fail and may lose precision. /// +/// Like `FromLossy`, implementations may lose precision. Like `TryFrom`, +/// conversions may fail on some input values. +/// /// Implementations should fail when the result type has no reasonable /// approximation of the input type and no appropriate special value (such as a /// representation of overflown or non-numeric values); otherwise, the /// conversion should succeed with an approximation of the input value. /// -/// For more precise definitions of "reasonable approximation" see the -/// documentation on the `FromLossy` trait. -pub trait TryFromLossy { +/// We do not specify the rounding mode used by implementations, but all results +/// where `try_from_lossy` does not fail and where the result is not a special +/// value should be numerically *close to* the input value. That is, if `x` is +/// the input value and `u` is the precision of the result type at `x` and +/// `Ok(r) = try_from_lossy(x)`, then it should normally hold that +/// `|x - try_from_lossy(x).unwrap()| < u`. The precision `u` may be constant +/// (as in integer types) or variable (as in floating point types) but should be +/// signficantly smaller than the magnitude of x (`u << |x|`) and should be +/// consistent with typical values for the result type. +pub trait TryFromLossy { type Error; - fn try_from_lossy(x: T) -> Result; + fn try_from_lossy(x: S) -> Result; +} +``` + +As with `TryFrom`, we propose to add a default implementation once +specialisation is available: +```rust +impl TryFromLossy for T where FromLossy: T { + default type Error = !; + default fn try_from_lossy(value: S) -> Result { + Ok(T::from_lossy(value)) + } } ``` @@ -184,87 +255,50 @@ value rounded towards zero. E.g.: - -0.2f32 → u32: 0 - 100_000f32 → u16: error -# Related problems +## Other conversions -These problems are discussed in the search for a complete solution; however it -is not currently proposed to solve them within this RFC. +### `WrappingFrom` trait -## Integer transmutations +There are several types of integer conversion which do not preserve the +semantic value: -There are several types of transmutation, discussed here separately, although -they are all transmutations. +- sign transmutation (i.e. reinterpretation as another integer type of the + same size but with different signed-ness), e.g. `128u8 → -128i8` +- unsigned truncation (simply dropping excess high bits), e.g. `260u16 → 4u8` +- signed truncation (dropping excess high bits and sign-transmuting the + remainder), e.g. `-260i16 → -4i8` -Note that there is less insentive against usage of `as` in these cases since the +Note that there is less incentive against usage of `as` in these cases since the conversions do not preserve "value", although alternatives still have some use (e.g. to clarify that a conversion is a truncation). -### Sign transmutations - -The conversions done by `as` between signed and unsigned types of the same size -are simply transmutations (reinterpretations of the underlying bits), e.g. -`0x80u8 as i8 == -128`. +We can add `std::convert::WrappingFrom`: -As these are not simple mathematical operations we could simply not provide any -alternative to `as`, and suggest usage of `mem::transmute` if necessary. - -Alternatively, we could add `transmute_sign` methods to all primitive integer -types, e.g.: ```rust -impl i32 { - ... - fn transmute_sign(self) -> u32 { ... } -} -``` - -### Unsigned truncation - -We could add `std::convert::TruncateFrom`: - -```rust -/// A trait for conversions which are truncate values by dropping unused high -/// bits. +/// A trait for wrapping conversions; these may truncate (drop high bits) and +/// sign-convert (re-interpret meaning of high bits). /// -/// Note that this is distinct from other types of conversion since high bits -/// are explicitly ignored and results are thus not numerically equivalent to -/// input values. -pub trait TruncateFrom { - fn truncate_from(x: T) -> Self; +/// Note that `TryFrom` and `WrappingFrom` may both be implemented for the same +/// source and target types; in this case they should have equivalent result +/// where `TryFrom` succeeds. +pub trait WrappingFrom { + fn wrapping_from(x: T) -> Self; } ``` -Add implementations for each unsigned integer type to each smaller unsigned -integer type. (See below regarding signed types.) - -Note that we *could* suggest users drop unwanted high bits (via masks or -bit-shifting) *then* use `TryFrom`, but this is a very unergonomic approach to -what is a simple and commonly used operation. - -### Signed truncation - -Bitwise operations on signed integers can have "unintuitive" results. For example, -```rust -fn main() { - let x = 3i32 << 14; - println!("{}", x); - println!("{}", x as i16); -} -``` -prints: -``` -49152 --16384 -``` -since the 16th bit is later interpreted as a -215 in the Two's -Complement representation, the numeric value on conversion to `i16` is quite -different despite all the dropped bits being 0. +Add implementations for all integer conversions which are *not implemented* by +`From`. These conversions should truncate or zero-extend to the size of the +result type, then reinterpret (transmute) the bits to the result type. -Essentially, operations like `i32` → `i16` are *shorten-and-transmute*. +Potentially this could also re-implement all `From` conversions on integer +types. This could be useful for generic code. Unfortunately since Rust has no +`Integer` trait there is no simple way to re-implement `From` *only* where the +source and target types are integer types. -Since these operations are not intuitive and not so widely useful, it may not -be necessary to implement traits over them. +# Related problems -Instead, we could suggest users implement signed truncations like this: -`x.transmute_sign().truncate_into::().transmute_sign()`. +These problems are discussed in the search for a complete solution; however it +is not currently proposed to solve them within this RFC. ## Platform-dependent types @@ -279,7 +313,7 @@ and that they could be larger than 64 bits. Checked integer conversions using `TryFrom` [are being reintroduced](https://github.com/rust-lang/rust/issues/49415). It is possible that unchecked conversions could be added, perhaps using -`TruncateFrom` (or some other trait allowing both truncation and +`WrappingFrom` (or some other trait allowing both truncation and zero-extension). # Drawbacks From 50b4204cfcdbfae1839cac31ff9c26763ea51854 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 30 Jun 2018 14:40:18 +0100 Subject: [PATCH 6/9] from-lossy: add RFC link --- text/0000-from-lossy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-from-lossy.md b/text/0000-from-lossy.md index d879a18752f..5f06eedbdc6 100644 --- a/text/0000-from-lossy.md +++ b/text/0000-from-lossy.md @@ -1,6 +1,6 @@ - Feature Name: from-lossy - Start Date: 2018-06-22 -- RFC PR: (leave this empty) +- RFC PR: https://github.com/rust-lang/rfcs/pull/2484 - Rust Issue: (leave this empty) # Summary From f7a08ce60b549e806b04bce57b9c8f771fced2a8 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 9 Jul 2018 13:14:37 +0100 Subject: [PATCH 7/9] from-lossy: make WrappingFrom support usize/isize --- text/0000-from-lossy.md | 47 ++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/text/0000-from-lossy.md b/text/0000-from-lossy.md index 5f06eedbdc6..00beb70aa7a 100644 --- a/text/0000-from-lossy.md +++ b/text/0000-from-lossy.md @@ -275,25 +275,41 @@ conversions do not preserve "value", although alternatives still have some use We can add `std::convert::WrappingFrom`: ```rust -/// A trait for wrapping conversions; these may truncate (drop high bits) and -/// sign-convert (re-interpret meaning of high bits). +/// A trait for wrapping type conversions with behaviour equivalent to the `as` +/// operator on integer types. /// -/// Note that `TryFrom` and `WrappingFrom` may both be implemented for the same -/// source and target types; in this case they should have equivalent result -/// where `TryFrom` succeeds. +/// Implementations convert values between fixed size integer types by +/// truncating, extending and re-interpreting the underlying bits: +/// +/// - if the target type is smaller than the source type, excess high bits are +/// dropped +/// - if the target type is larger than the source type then, for unsigned +/// source types, the value is zero-extended; for signed source types, the +/// value is sign-extended +/// - once the value has the same size as the target type, it is reinterpreted +/// +/// Equivalently, the conversion results can be defined numerically (where `n` +/// is the initial value and `M` is the number of distinct values in the target +/// type): +/// +/// - for unsigned target, `n % M` +/// - for signed target, `if n % M < M/2 { n % M } else { n % M - M }` pub trait WrappingFrom { fn wrapping_from(x: T) -> Self; } ``` -Add implementations for all integer conversions which are *not implemented* by -`From`. These conversions should truncate or zero-extend to the size of the -result type, then reinterpret (transmute) the bits to the result type. +Add implementations `impl WrappingFrom for V` for all pairs of integer types +`U, V` (including where `U = V`, and including the platform-dependent types +`usize` and `isize`). + +This has overlap with the `From` and `TryFrom` traits. To quote @scottmcm: -Potentially this could also re-implement all `From` conversions on integer -types. This could be useful for generic code. Unfortunately since Rust has no -`Integer` trait there is no simple way to re-implement `From` *only* where the -source and target types are integer types. +> I believe it should have all pairs for the integer types, because a wrapping +> conversion from u8 to u16 has perfectly-well defined semantics. (Though I'd +> also like a clippy lint suggesting `from` instead where possible.) And I think +> it could be useful for machine-generated code as mentioned in +> [#2438 (comment)](https://github.com/rust-lang/rfcs/pull/2438#issuecomment-403255258). # Related problems @@ -312,9 +328,10 @@ and that they could be larger than 64 bits. Checked integer conversions using `TryFrom` [are being reintroduced](https://github.com/rust-lang/rust/issues/49415). -It is possible that unchecked conversions could be added, perhaps using -`WrappingFrom` (or some other trait allowing both truncation and -zero-extension). +It remains to be decided whether `From` should support conversions such as +`From for u32` on some platforms only, or not at all. For this reason, +the platform-dependent types are currently also excluded from the `FromLossy` +trait. # Drawbacks [drawbacks]: #drawbacks From f89b3bec06c9c4213090a3d24219e68fe11b14b9 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 13 Aug 2018 08:27:30 +0100 Subject: [PATCH 8/9] from-lossy: clarify std/core module location and error type --- text/0000-from-lossy.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/text/0000-from-lossy.md b/text/0000-from-lossy.md index 00beb70aa7a..778f5607471 100644 --- a/text/0000-from-lossy.md +++ b/text/0000-from-lossy.md @@ -68,6 +68,9 @@ Type conversions can be handled by the following traits: # Reference-level explanation [reference-level-explanation]: #reference-level-explanation +This concerns traits in the `std::convert` module: the existing `From` and +`TryFrom`, as well as some new traits. + ## (Approximate) semantic value preserving conversions ### `From` trait @@ -233,7 +236,7 @@ impl TryFromLossy for T where FromLossy: T { } ``` -Add implementations from all of: +Additionally, define implementations from all of: - `f32, f64` @@ -255,6 +258,16 @@ value rounded towards zero. E.g.: - -0.2f32 → u32: 0 - 100_000f32 → u16: error +These implementations should use the following error type, defined in +`std::num` (as noted by @SimonSapin, this allows modification later): + +```rust +struct TryFromFloatError{ + _dummy: () +}; +``` + + ## Other conversions ### `WrappingFrom` trait @@ -311,6 +324,13 @@ This has overlap with the `From` and `TryFrom` traits. To quote @scottmcm: > it could be useful for machine-generated code as mentioned in > [#2438 (comment)](https://github.com/rust-lang/rfcs/pull/2438#issuecomment-403255258). +## Core lib + +`core` is the subset of `std` which is applicable to all supported platforms. +Since it already includes all types mentioned here as well as `From` and +`TryFrom`, the traits and implementations proposed by this RFC should in fact +be placed in the `core` lib with aliases in `std`. + # Related problems These problems are discussed in the search for a complete solution; however it From 069c6426c5fe7ac9b776b6bf094a0db89a769164 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 13 Aug 2018 08:28:21 +0100 Subject: [PATCH 9/9] from-lossy: allow conversion from usize/isize to float types --- text/0000-from-lossy.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/text/0000-from-lossy.md b/text/0000-from-lossy.md index 778f5607471..bb22848e4b8 100644 --- a/text/0000-from-lossy.md +++ b/text/0000-from-lossy.md @@ -183,15 +183,20 @@ pub trait FromLossy { Add implementations to `f64` from all of: -- `u64, i64, u128, i128` +- `u64, i64, u128, i128, usize, isize` and to `f32` from all of: -- `f64, u32, i32, u64, i64, u128, i128` +- `f64, u32, i32, u64, i64, u128, i128, usize, isize` These conversions should round to the nearest representable value, with ties to even (as is commonly used for floating-point operations). +Note that for the platform-dependent types `usize` and `isize`, some conversions +will always be exact on some platforms (i.e. to `f64` on 32-bit platforms, and +to both float types on 16-bit platforms). We consider this acceptable despite +overlap with a potential `From` implementation in such cases. + (Note: other integer → float conversions are already handled by `From` since they are loss-less. There is a question below about trait overlap.)