From eea19ffe87e8e9e0798cad8205dbc0823246aeaf Mon Sep 17 00:00:00 2001 From: Victor Polevoy Date: Fri, 7 Apr 2023 13:25:42 +0200 Subject: [PATCH 1/3] Start the Range value and value conversion RFC. --- text/0000-range-get-and-convert-methods.md | 345 +++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 text/0000-range-get-and-convert-methods.md diff --git a/text/0000-range-get-and-convert-methods.md b/text/0000-range-get-and-convert-methods.md new file mode 100644 index 00000000000..95fc8931722 --- /dev/null +++ b/text/0000-range-get-and-convert-methods.md @@ -0,0 +1,345 @@ +- Feature Name: `std::ops::Range/RangeInclusive::get_value and conversion methods`) +- Start Date: 2023-04-07 +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) + +# Summary +[summary]: #summary + +Add a method to get a value from range, which is tied to the range. +Later that value may be converted into another range of values, +preserving the relative position within the new range. + +# Motivation +[motivation]: #motivation + + + +It is useful to have a range of possible values and being able to quickly +obtain a value from this range. Currently, there is no way to get a value +from a range of types `std::ops::Range` and `std::ops::RangeInclusive`; +there is only a method called `contains()` which can be called with a +proposed value to check if it lies within the range. Later, if the +value lies within the range, there is no way to tell that the value +checked actually does that within the code: additional logic is required. + +A possibility for a value to be tied to a "parent" range it was got from +will allow a value-to-new-range conversion. For example, we may want to +have a thread priority value, which we may want to be "user-friendly" by +having values in the range of `[0; 100]`. Later, we may pick a value out +of this range, for example, `50`. However, on different operating systems +the thread priority ranges are different and depend on many things; in +other words, it is almost certainly not the `[0; 100]` range we wanted. +Let's assume we want to change a Linux niceness of a thread. On Linux, +the niceness values are in the range of `[-20; 19]`. A certain calculation +is required to map a value `50` from range `[0; 100]` to the range +`[-20; 19]`, to preserve the relative (middle) position, which would be +`0` in this case (`40` allowed values in total). This can be avoided +as these calculations can be all written once and just used. Such a +mechanism within an already existing type like `std::ops::Range` and/or +`std::ops::RangeInclusive` would greatly simplify this process of +mapping values from certain ranges. + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + + + +By introducing a new type called `RangeValue`, we can have a type which +declares a value tied to its parent range it was taken from: + +```rust +use std::ops::Deref; + +/// A Range value which is tied to the range object and its lifetime. +#[derive(Debug, Copy, Clone)] +struct RangeValue<'r, V> { + value: V, + range: &'r std::ops::Range, +} +impl<'r, V> RangeValue<'r, V> + { + fn get(&self) -> &V { + &self.value + } + + fn range(&self) -> &std::ops::Range { + &self.range + } +} + +impl<'r, V> AsRef for RangeValue<'r, V> { + fn as_ref(&self) -> &V { + self.get() + } +} + +impl<'r, V> Deref for RangeValue<'r, V> { + type Target = V; + + fn deref(&self) -> &Self::Target { + self.get() + } +} +``` + +Such a value will always be known as a value which lies within the range +and this fact will never be "forgotten" within the code, as it is can be +"promised" and ensured at compile time. + +By introducing a new method called `get_value()` to both, `std::ops::Range` and +`std::ops::RangeInclusive`, it becomes possible to get a value tied to +the range it was taken from: + +```rust +trait GetRangeValue<'r, V> where V: ToOwned { + /// Returns a [`RangeValue`] if it lies within the range, otherwise, + /// [`None`]. + /// + /// The returned value is bound to this Range. + fn get_value(&'r self, v: &V) -> Option>; +} + +impl<'r, V> GetRangeValue<'r, V> for std::ops::Range where V: ToOwned + PartialEq + PartialOrd { + fn get_value(&'r self, v: &V) -> Option> { + if self.contains(v) { + Some(RangeValue { + value: v.to_owned(), + range: &self, + }) + } else { + None + } + } +} +``` + +Later we introduce a method for `RangeValue` which would convert the +value from one range to another range's value: + +```rust +use std::ops::{Add, Sub, Div, Mul}; + +impl<'r1, 'r2, V> RangeValue<'r1, V> + where + V: Copy + ToOwned + Sub + Mul + Div + Add + PartialEq + PartialOrd, +{ + /// Convert into another range of values while preserving the relative + /// position. + fn convert(&'r1 self, range: &'r2 std::ops::Range) -> Option> + { + let out_range: V = range.end - range.start; + let in_range: V = self.range.end - self.range.start; + let new_possible_value: V = (self.value - self.range.start) * out_range / in_range + range.start; + range.get_value(&new_possible_value) + } +} + +fn main() { + // This provides a value tied to its parent range it is taken from. + // This value is tied to its range's lifetime. + let range = 0..10; + let value = range.get_value(&5); + assert_eq!(value.unwrap().get(), &5); + assert_eq!(*value.unwrap(), 5); + + // This successfully converts a value `5` from range `0..10` to + // the range of `0..100`, which would be equal to `50`. + let new_range = 0..100; + let value = value.unwrap().convert(&new_range); + assert_eq!(value.unwrap().get(), &50); + + // This value is out of scope of the allowed values, so a `None` + // value is returned. + let value = new_range.get_value(&500); + assert!(value.is_none()); +} +``` + +The value returned from the range is guaranteed to lie within the allowed +range of values represented by the range the method is used on. + +This feature would allow to: + +1. Easily know whether a value is within some range or not and such fact + will never be able to be "forgotten" in the code as the `RangeValue` + types make sure it is bound to the parent range and this is ensured + at compile time. +2. Easily map a value from one range to another range, preserving the + relative position within the range. + +In the end, the feature brings: + +1. A new type `RangeValue` which defines a type bound to a range. +2. A new trait `GetRangeValue` implementors of which return a +`RangeValue`. +3. A new way to obtain a value from a range based on the `Option` type: +when `Some` is returned, a value returned is guaranteed to lie +within the range and it can't change. As opposed to using the +`contains()` method, this allows the developer to work with a type +having a guarantee that this value can't be changed and lies within +the scope of allowed values by the range, and this fact can't be +forgotten or abused in the code. It also brings a slightly more +convenient way of getting a value from the range. Consider a use-case +when a `Result` type is used. Now it is possible to use the `try!` macro +or the "question-mark" operator `?` to quickly exit the function when a +value doesn't lie within the range: + + ```rust + fn set_thread_priority(priority: u8) -> Result<(), &'static str> { + let value = (0..100) + .get_value(&priority) + .ok_or_else(|| "The priority doesn't lie within the user-allowed range")?; + + // The same value but mapped to the allowed values range for + // the niceness: + let mapped = value.convert(&(-20..20)) + .ok_or_else(|| "The priority doesn't lie within the niceness range")?; + + set_niceness_for_current_thread(*mapped) + } + ``` + + + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +This is the technical portion of the RFC. Explain the design in sufficient detail that: + +- Its interaction with other features is clear. +- It is reasonably clear how the feature would be implemented. +- Corner cases are dissected by example. + +The section should return to the examples given in the previous section, and explain more fully how the detailed proposal makes those examples work. + +# Drawbacks +[drawbacks]: #drawbacks + + +No known and reasonable drawbacks. + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +- Why is this design the best in the space of possible designs? + +It is a simple as possible. Should also be fast enough. + +- What other designs have been considered and what is the rationale for not choosing them? + +For the sake of this RFC, a special trait has been provided to allow the +readers to understand how it is supposed faster, by providing a +fully-working code; for the implementation we may avoid using traits in +favour of using struct methods. + +- What is the impact of not doing this? + +When it comes to conversion of a value from one range to another, - +everyone who needs to perform the same operation will have to spend time +googling and calculating everything on his own, possibly doing mistakes. + +When it comes to improving usability of the Range structures, this RFC +suggests a way to guarantee a certain value lies within the range by +providing a specific type, which is supposed to only be created by a +range object when a value lies within the range. By having it as a +separate type with lifetime bounds to its parent range and the ability +not only be immutable, the developer never has to guess and carefully +re-read the code to understand he did the things right. + +When it comes to the interface, returning an `Option` when getting the +range value lying within the range allows to easily use the question-mark +operator `?` to greatly simplify the workflow when any compatible type +used (which implements the `std::ops::FromResidual` trait). + +When converting a value from one range to another, the calculated value +should lie within the range, but it may not be when the new range to +which the mapping was done is empty. To handle this case, the `Option` +is also used the same way. + + +# Prior art +[prior-art]: #prior-art + +Discuss prior art, both the good and the bad, in relation to this proposal. +A few examples of what this can include are: + +- For language, library, cargo, tools, and compiler proposals: Does this feature exist in other programming languages and what experience have their community had? + +I don't know that. + +- For community proposals: Is this done by some other community and what were their experiences with it? + +I am not aware of that. + +- For other teams: What lessons can we learn from what other communities have done here? + +I don't know. + +- Papers: Are there any published papers or great posts that discuss this? If you have some relevant papers to refer to, this can serve as a more detailed theoretical background. + +I am not aware of this. + +This section is intended to encourage you as an author to think about the lessons from other languages, provide readers of your RFC with a fuller picture. +If there is no prior art, that is fine - your ideas are interesting to us whether they are brand new or if it is an adaptation from other languages. + +Note that while precedent set by other languages is some motivation, it does not on its own motivate an RFC. +Please also take into consideration that rust sometimes intentionally diverges from common language features. + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +- What parts of the design do you expect to resolve through the RFC process before this gets merged? + +I suggest to get rid of the `trait GetRangeValue` used in this RFC in +favour of having `std::ops::Range` and `std::ops::RangeInclusive` +methods instead. + +I also suggest to carefully think about the naming of the methods and +types used for this RFC. + +- What parts of the design do you expect to resolve through the implementation of this feature before stabilization? + +All the corner-cases when it comes the value calculation: if we can +guarantee that the new range to which the mapping is done can't be empty +and is always valid, we may avoid returning `Option` from there. + +- What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC? + +Don't know. + +# Future possibilities +[future-possibilities]: #future-possibilities + + + +Perhaps, it makes sense to implement the `std::iter::FromIterator` trait +for the `RangeValue` type, so that it could create a new `Range` based +on the values collected: the lowest value is the start of the new range +and the highest value is the end. I am not sure how useful this is +though. + + From b2f28ae2bae3c5a39559ef061d3f424b8ec4eb39 Mon Sep 17 00:00:00 2001 From: Victor Polevoy Date: Fri, 7 Apr 2023 13:40:53 +0200 Subject: [PATCH 2/3] Update 0000-range-get-and-convert-methods.md Typos/rephrasing --- text/0000-range-get-and-convert-methods.md | 44 ++++++++++++---------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/text/0000-range-get-and-convert-methods.md b/text/0000-range-get-and-convert-methods.md index 95fc8931722..594c0627baf 100644 --- a/text/0000-range-get-and-convert-methods.md +++ b/text/0000-range-get-and-convert-methods.md @@ -6,7 +6,7 @@ # Summary [summary]: #summary -Add a method to get a value from range, which is tied to the range. +Add a method to get a value from the range tied to the range. Later that value may be converted into another range of values, preserving the relative position within the new range. @@ -15,10 +15,10 @@ preserving the relative position within the new range. -It is useful to have a range of possible values and being able to quickly +It is helpful to have a range of possible values and be able to quickly obtain a value from this range. Currently, there is no way to get a value from a range of types `std::ops::Range` and `std::ops::RangeInclusive`; -there is only a method called `contains()` which can be called with a +there is only a method called `contains()`, which can be called with a proposed value to check if it lies within the range. Later, if the value lies within the range, there is no way to tell that the value checked actually does that within the code: additional logic is required. @@ -27,15 +27,15 @@ A possibility for a value to be tied to a "parent" range it was got from will allow a value-to-new-range conversion. For example, we may want to have a thread priority value, which we may want to be "user-friendly" by having values in the range of `[0; 100]`. Later, we may pick a value out -of this range, for example, `50`. However, on different operating systems +of this range, for example, `50`. However, on different operating systems, the thread priority ranges are different and depend on many things; in other words, it is almost certainly not the `[0; 100]` range we wanted. Let's assume we want to change a Linux niceness of a thread. On Linux, the niceness values are in the range of `[-20; 19]`. A certain calculation -is required to map a value `50` from range `[0; 100]` to the range +is required to map a value `50` from the range `[0; 100]` to the range `[-20; 19]`, to preserve the relative (middle) position, which would be `0` in this case (`40` allowed values in total). This can be avoided -as these calculations can be all written once and just used. Such a +as these calculations can all be written once and just used. Such a mechanism within an already existing type like `std::ops::Range` and/or `std::ops::RangeInclusive` would greatly simplify this process of mapping values from certain ranges. @@ -114,7 +114,7 @@ impl<'r, V> GetRangeValue<'r, V> for std::ops::Range where V: ToOwned + +TODO # Drawbacks [drawbacks]: #drawbacks @@ -238,23 +242,23 @@ It is a simple as possible. Should also be fast enough. - What other designs have been considered and what is the rationale for not choosing them? For the sake of this RFC, a special trait has been provided to allow the -readers to understand how it is supposed faster, by providing a -fully-working code; for the implementation we may avoid using traits in +readers to understand how it is supposed to work faster, by providing a +fully-working code; for the implementation, we may avoid using traits in favour of using struct methods. - What is the impact of not doing this? -When it comes to conversion of a value from one range to another, - +When it comes to the conversion of a value from one range to another, - everyone who needs to perform the same operation will have to spend time googling and calculating everything on his own, possibly doing mistakes. -When it comes to improving usability of the Range structures, this RFC +When it comes to improving the usability of the Range structures, this RFC suggests a way to guarantee a certain value lies within the range by providing a specific type, which is supposed to only be created by a range object when a value lies within the range. By having it as a separate type with lifetime bounds to its parent range and the ability not only be immutable, the developer never has to guess and carefully -re-read the code to understand he did the things right. +re-read the code to understand he did things right. When it comes to the interface, returning an `Option` when getting the range value lying within the range allows to easily use the question-mark @@ -289,18 +293,20 @@ I don't know. I am not aware of this. + # Unresolved questions [unresolved-questions]: #unresolved-questions - What parts of the design do you expect to resolve through the RFC process before this gets merged? -I suggest to get rid of the `trait GetRangeValue` used in this RFC in +I suggest getting rid of the `trait GetRangeValue` used in this RFC in favour of having `std::ops::Range` and `std::ops::RangeInclusive` methods instead. @@ -309,7 +315,7 @@ types used for this RFC. - What parts of the design do you expect to resolve through the implementation of this feature before stabilization? -All the corner-cases when it comes the value calculation: if we can +All the corner-cases when it comes to the value calculation: if we can guarantee that the new range to which the mapping is done can't be empty and is always valid, we may avoid returning `Option` from there. From eb161becb006bf2e5d9bb65faa6cc16394ffbd27 Mon Sep 17 00:00:00 2001 From: Victor Polevoy Date: Fri, 7 Apr 2023 14:06:17 +0200 Subject: [PATCH 3/3] Mention the flaw of losing precision. --- text/0000-range-get-and-convert-methods.md | 48 +++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/text/0000-range-get-and-convert-methods.md b/text/0000-range-get-and-convert-methods.md index 594c0627baf..9baf2b17ede 100644 --- a/text/0000-range-get-and-convert-methods.md +++ b/text/0000-range-get-and-convert-methods.md @@ -56,6 +56,7 @@ use std::ops::Deref; struct RangeValue<'r, V> { value: V, range: &'r std::ops::Range, + initial: Option, } impl<'r, V> RangeValue<'r, V> { @@ -306,7 +307,7 @@ Please also take into consideration that rust sometimes intentionally diverges f - What parts of the design do you expect to resolve through the RFC process before this gets merged? -I suggest getting rid of the `trait GetRangeValue` used in this RFC in +I suggest getting rid of the trait `GetRangeValue` used in this RFC in favour of having `std::ops::Range` and `std::ops::RangeInclusive` methods instead. @@ -319,6 +320,51 @@ All the corner-cases when it comes to the value calculation: if we can guarantee that the new range to which the mapping is done can't be empty and is always valid, we may avoid returning `Option` from there. +Another problem which might happen when converting values from one range +to another and back or just multiple times is losing precision in +terms of the initial relative position. For example, when converting a +value of `50` from the range `[0; 100]` to the range of `[1; 3]`, +the conversion back won't work as expected: + +```rust + let new_range = 0..100; + let value = value.unwrap().convert(&new_range); + assert_eq!(value.unwrap().get(), &50); + + // After this conversion, the new value will lose the precision of + // the initial value relative position. + let new_range = 1..4; + let value = value.unwrap().convert(&new_range); + assert_eq!(value.unwrap().get(), &2); + + let new_range = 0..10; + let value = value.unwrap().convert(&new_range); + // This assertion fails, the value converted is actually `3`. + assert_eq!(value.unwrap().get(), &5); +``` + +I can't think of any **easy** way to circumvent this, so, probably, it should +just be mentioned in the documentation that this should be expected. +The only thing promised should be that the calculated value lies within +the new range. When it comes to losing the precision, the smaller the +range to which conversion is performed, the smaller the precision +will be when converting this value to a bigger range. + +However, there still is a solution to that problem. Within the +`RangeValue` struct we can additionally store an `Option` which +would store the initial (and so of maximum precision possible) range +value, and use it instead of the "current" one. This way, in the +example above, we can safely calculated the value `5` in the last +assertion, as the conversion would be done from the initial range value: +`50` from the range of `[0; 100]`, rather than of `2` from the range of +`[1; 3]`. + +Another possible improvement is that the `Range` object may occupy less +space than a reference to it. So, depending on the size of the `Idx` +type used for the `Range`, we may decide whether to use an implementation +storing references to the parent `Range` in the `RangeValue` or a full +copy of it instead if it occupies less space. + - What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC? Don't know.