My least favorite Rust type
ridiculousfish.com
ridiculousfish.com
Even this needn't be the case. `Range` can implement `IntoIter` to plug into `for` loop syntax.
Another related problem is that `SliceIndex` (https://doc.rust-lang.org/std/slice/trait.SliceIndex.html) trait, which is used to implement indexing, is perma-unstable. So, even if you build your own better range, you can't make it play nicely with slices.
Now consider the usability nightmare `(1..10).unwrap()` would be and `&slice[(1..10).unwrap()]` and `(1..10).and_then(|range| slice.get(range))` instead of `slice.get(1..10)`.
That's the actual problem, not that `Range` implements iterator.
Oh and most ranges are used ad-hoc (created and then directly consumed) so for many use cases going with Range + IntoIterator would increase the overhead.
Besides that while SliceIndex is perma unstable, `Index` is not so if you control the container you can make it work alternatively you can always do `my_range.index(slice)`.
(Of course unlike x+y, x..y would require checks in release builds too.)
Although, it'll require some non-minor api adjustments: at the moment, Range's fields are public. To maintain the invariant, we would have to make them private and provide getters. Which we actually already do for RangeInclusive, because of that extra bool field. Which is an inconsistent mess :)
error: this range is empty so it will yield no values
--> src/main.rs:2:9
|
2 | let x = 5..0;
| ^^^^
|
= note: `#[deny(clippy::reversed_empty_ranges)]` on by default
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#reversed_empty_ranges
help: consider using the following if you are attempting to iterate over this range in reverse
|
2 | let x = (0..5).rev();
| ^^^^^^^^^^^^special casing a check on literals feels a bit like saying you can create an empty non-mutable vector.
I continuously really want to like the language. The premise is good, a lot of the ideas are really compelling, but when it comes down to aspects I disagree with, I get a strong sense of Rust demanding that users subjugate themselves to all the choices and opinions of the architects, and is (are?) hostile to the notion of a user expressing themselves through their tools, and not just their output. As a result, every time I interact with Rust my hackles end up getting raised and so far it would seem that a (currently absent) existential motivator would be required for me to get past it.
Even without introducing a new type, it could be improved in a backwards compatible way. For example, derive the Copy trait, and add a contains method that takes an owned value instead of a reference (at least as long as the Idx type is Copy).
Here is the discussion
> In 2021 edition the compiler team could change the
meaning of (3..5) to resolve to a new type. Then if you wanted
the old type you have to request it by a fully qualified name
or something.
And the responses was: > That would be pretty awful. I think it would be
better to change IntoIterator and warn for differences
in copy and intoiterator in both editions so you can write portable code.I've basically decided to stay in my (ever-improving) comfort zone of C++ until Rust gets named/default arguments, which is my arbitrary litmus test for whether it's going to actually be a usable language for me.
fn f(n: Option<u8>) -> u8 {
match n {
Some(n0) => n0,
None => 10
}
}
fn main() {
let n1 = f(Some(20));
let n2 = f(None);
println!("{} {}", n1, n2);
}1: https://dave.cheney.net/2014/10/17/functional-options-for-fr...
I normally just leverage a couple of features: Derive(default) to have an "empty" initialized value and the struct field filling syntax
let x = Foo { field_i_care_about, ..Default::default() }; struct OptionsStruct {...}
impl Default for OptionsStruct {...}
impl Add for OptionsStruct {...}
fn port(portnum : u16) -> OptionsStruct {...}
fn timeout(millis: u64) -> OptionsStruct {...}
fn maxconns(conns: usize) -> OptionsStruct {...}
fn main() {
...
// startServer("localhost", default())
startServer("localhost", port(54321) + timeout(1000));
...
}
Not the most ergonomic, I know, and requires custom architecting for each instance of this, but somewhat doable.I don't really get where you're coming from here, especially with regards to this issue -- it seems like almost everyone, including Rust core team members, agree that `impl Iterator for Range` was an API design mistake, that came out of (IIRC) Iterator existing prior to IntoIterator in the pre-1.0 days. This seems to me to just be an unfixable design flaw due to backwards-compatibility rules, not something that really speaks to the design of the language as a whole.
This sounds like something that could be solved in the next edition.
I don't know why you feel that way. Any major language or standard library change goes through an RFC process, where anyone in the community can submit feedback, and from what I have seen, the official teams are very responsive to concerns.
- The big one is that, at least what I see from my admittedly flawed perspective, there appears to be an active annoyance by a significant number of participants on the various issues on the importance of maintaining library ergonomics under constrained execution environments, with bare metal / free-standing environments being an obvious scenario. An example that comes to mind being stuff dealing with situations where 'global allocation' is actively harmful, which, in addition to happening in kernel development, also happens when building managed runtimes.
- What comes across as an almost begrudging foreign-function interface, no ABI commitments, and lack of clarity about what is undefined behaviour under the `unsafe` operation needed to engage in any of this
- Cargo VS rustc and integration into other build systems. Sanctioned VS unsanctioned paths more generally.
- Prominent people in the community making comments about actively suppressing individual stylistic preferences because it's better for the collective
The rest of the issues you mentioned are being actively worked on or investigated. Yes Rust 1.0 didn't solve all problems out of the gate and even now there's a lot of work still to do. But a lot of progress has been made and is being made.
I get that if you don't follow issues, working groups, zulip chats, etc that it may not be obvious what is and isn't being worked on but I'm really not sure where you're getting this "arrogance" from.
I would argue that this is because there are differences in the severity of the tooling. gofmt for example will not override your decisions regarding line breaks last time I checked, while rustfmt will not afford you any freedoms there.
My understanding is that this is being worked on. Of course any language (or really any project) will have people annoyed that the maintainers priorities don't align perfectly with their priorities.
> What comes across as an almost begrudging foreign-function interface,
I haven't gotten any impression that FFI is begrudging. It really seems like a first class citizen to me, and is certainly better than the FFI story in say, java or go. Rust even has language features that exist just for FFI, such as unions and c-style variadic arguments.
> no ABI commitments
This is a recognized problem. It is also a hard problem, since lifetimes don't exist after compile time. But using the C-ABI is quite usable in some cases. And there have been a few RFCs around this recently (specifically around stable ABIs for vtables).
> lack of clarity about what is undefined behaviour under the `unsafe` operation needed to engage in any of this
This is again a known problem, and something that is improving.
> Cargo VS rustc and integration into other build systems.
I think there was some work being done on this, but I don't know the current status. There are people that use rust with bazel.
Since 1.0 Rust has improved a lot while maintaining compatibility, even through a big deprecation step like the 2018 edition.
A good chunk of those improvements are about making Rust more usable and more forgiving. Some were things I didn't initially recognise as usability problems, but what came out the other side was absolutely better – the modules changes are a good example.
Rust's lead contributors have been welcoming and humble – while also being human. It can be very frustrating running an open source project, and Rust is trying very hard to be open and inclusive, which increases the challenge.
When writing Rust, there are definitely paths of least resistance. You often read about the moment people realise they're going "against the grain", and discovering why it's safar and/or faster to do it "the Rust way". But that's not subjugation. It's about understanding your tools, how they work, and what they're for.
Can you point us to where this antipathy was displayed?
That being said, I can understand why this might not necessarily be desirable for some people; Copy is generally reserved for types that are "small enough" that implicitly copying the bytes will be cheap enough to ignore. It might seem obvious that a struct containing two types that are Copy should also be cheap enough to be Copy, the line has to be drawn _somewhere_, and for any number of bytes that is chosen to be the threshold where the cost is too high, it's pretty easy to pick a size that's below that threshold but a struct containing two instances of that size is above the threshold. Because of the unclear boundaries for that sort of thing, I generally tend only to derive Copy on things in my code that thin wrappers around existing types that are Copy, e.g. `struct Foo(i32)`.
https://github.com/rust-lang/rust/issues/48649#issuecomment-...
Adding a `Copy` constraint seems a bit weird to me, since I expect those borrows (e.g. on `contains`) for consistency with collections. Deriving `Copy` (i.e. implementing it when the index type is `Copy`) should be enough.
One small mistake in the article is that you need a `PartialOrd` constraint not a `PartialEq` constraint to ensure start <= end.
https://github.com/bluejekyll/trust-dns/blob/main/crates/ser...
It's also not possible to call methods like contains() on Range<Idx> unless the type is (Partial?)Ord, because of the constraint on Idx in the impl which defines those methods.
He claims that language designers should aim for "80% solutions" instead, which cover most common usages but limit themselves enough to avoid complexity. This runs in contrast to a lot of commonly accepted language design wisdom.
`Range` was discussed a lot before being stabilized and it's drawbacks where well known when it was stabilized.
The reason it was stabilized that way anyway was because it happens to work out best for the most common use-cases.
It's more of a "practically use-full but theoretically imperfect compromiss" thing.
The main usage of range are:
1. To iterate over it
2. To slice things using it
Which is what it is focused on in it's design.
Sure ranges could be `Copy` but one of their main purposes is to be an iterator so it's reasonable to not make them copy as that would be a usability nightmare.
Sure it's strange that you can construct a invalid range and then panic when you use it to slice something. But the alternative would be to make the creation of range fallible which is a usability nightmare. Furthermore validity depends on what you use it one, so a backwards range might be a very reasonable thing for some use-cases so practically it's best to make every `Range` valid, but not necessary every usage of one.
Sure exclusive ranges based on start+end can't contain the maximal value but that's a fundamental property of exclusive ranges defined through start+end. There is a reason mathematics have 4 kind's of ranges (differing in exclusiveness in start/stop).
Sure `.contains` takes a reference, but that's a problem about how rust can't specialize traits in how they need references for Copy methods. Not having that would prevent the usage of ranges of `BigNums` or similar reasonable usages.
Sure `RangeInclusive` could be made shorter but that also means you can't have empty inclusive ranges and you can't use it directly as an iterator, which is probably the most common use case of inclusive ranges.
All in all the `Range` types are a compromise optimized for it's most common use cases. That makes some parts sub-optimal if used for other cases but you can also always use your own types so that's not really a problem in practice.
Also he does some mistakes:
- You can't enforce `start <= end` at construction time without making the constructor fallible which would be a ergonomic nightmare. Which means that neither `[T]::get()` or `Range.len()` would get faster nor would is_empty get easier.
The last point also sadly means that for certain arithmetic high performance tasks it can make sense to not use the rust provided range type but a custom one.
In reality, Range of some T generally makes sense in a local API or program. Even if that same Range<T> doesn't necessarily make sense in every other place T might be used.
I'm not sure what malformed return values this is referring to, because I can't think of any. Is it referring to the fact that ranges where the start is greater than the end will result in an empty range? Without dependent types, which Rust doesn't have, there's no way to detect that; even in the subset of cases where the range bounds are computable at compile-time, back at 1.0 Rust didn't have the compile-time evaluation machinery necessary to make that happen. You could instead choose to interpret that a range where the start is greater than the end indicates a descending range, but plenty of other people will regard that behavior as a flaw.
- len is `ExactSizedIterator.len()` which is the length of `Range` as iterator, i.e. the number of items yielded by next. Which is 0.
- When slicing with 5..0 it threats it not as an empty iterator but as an out of bounds access. This is without question slightly inconsistent and not my favorite choice but was decided explicitly this way as it makes it much easier to catch bugs wrt. wrongly done slices. Also it only panics if you do Index which can panic anyway but it won't panic if you use e.g `get` where it return `None` so making it traet the "bad" empty case differently for slicing doesn't add a new error path, but doing so for iteration and `len` would add a new error path especially given that `ExactSizedIterator.len()` isn't supposed to panic as it's a size hint.
If you index a slice with a out of bounds index it will panic independent of weather the index is a usize or a Range<usize>.
If you use `get` with a out of bound index you always get a None.
Sure it's open for discussion if why a range with start > end should be treated the same as an out of bounds index or if it should be treated as empty slice. But then doing the former makes it easier to catch errors.
Enforcing start <= end would mean that the range construction is fallible which would be a major usability nightmare and now you would need two synatxes one for the normally error handling and one for panicking or you would need to add a lot of unwraps or similar.
Range's are mainly used ad-hoc (e.g. `slice[start..=mid+2]`) or `for x in x..y {...}` and are optimized for that usage patterns.
For other usages they might not be optimal. But you can always do your own types.
It's not the case. The only think affected by range being generic is that `contains` takes a reference instead of a copy (which btw. can likely be eliminated by the optimizer). Which is necessary to allow thinks like `Range<BigNum>`.
All other things have nothing to do with it being generic but with for which use cases it was designed for.
In the end in rust a Range is mainly an iterator.
If it's a Range<usize> and only then you can also use it to get slice arrays/vectors/slices.
Which means that e.g. the unstable experimental `get_unchecked` function is actually very well defined.
Lastly the reason why you can't enforce `start <= end` is because that would make the creation of an range fallible which would be a horrible usability nightmare, a thing the author somehow misses completely.
The thing is indexing a slice already can panic so moving the panic there is generally a good idea. Similar you always want to have a non-panic path. Which would be e.g. `[T]::get()` which in case of a "bad" slice does the same as on a "bad" index it returns `None`.
In the end both `Range` and `RangeInclusive` are compromises focused on the most common use cases of range, which is a ad-hoc creation "just around" the place you consume it for iteration or slicing of slices. Which also means that e.g. the fact that `RangeInclusive` is bigger is no problem as at the place it's used you elsewise would need to either turn it into a iterator just like `RangeInclusive` adding even more overhead then the current `RangeInclusive`. Sure if you want to store a lot of `RangInclusive`s then this is not the use-case it was defined for and you are better of defining your own range inclusive.
This is easy enough to say, and indeed I do think it's a good approach, but the problem is identifying that 80% in the first place. The reason that language designers tend to favor general approaches is because they presume not to know how people are going to want to use certain things. It's an approach borne out of humility, not ideology. You need time observing how things are used in the wild before you can identify which 20% not to support; get this wrong and people will be more frustrated than if you had saddled them with the baggage of the general approach.
In the specific case of Rust's Range API, we can observe this problem acutely. Rust hugely benefited from the period between 2011 and 2015 where it was able to iterate aggressively on design and observe what opinionated stances were worthwhile. But the Range type came relatively late to the party: it was devised and stabilized only months before 1.0 as a replacement for an old, hardcoded slicing syntax that worked with no types other than plain integers, and only in very limited syntactic contexts. With little time to observe use in the wild (and with all the other madness and work that was going on in the run-up to 1.0), the reasonable approach was to not over-constrain. Now that we have experience with it one could devise ways to do it better, certainly, and with luck Rust may be able to move the type in that direction, but other than that it may just be a lesson for those languages that are yet to come.
Given a new language feature and limited time to observe actual use, IMHO the reasonable approach would be to constrain it as tightly as possible. It's much easier to relax constraints to enable new uses later than it is to reign in inadvisable uses of an underconstrained interface. For example, if the original Range interface had simply consisted of two private, immutable fields with Copy + PartialOrd constraints and an implementation of the IntoIterator trait then it would be trivial to add setters (or public fields), an internal Iterator implementation, and looser type constraints later on if these were deemed necessary. Going the other way, however, breaks programs that have come to depend on these dubious features.
A good language designer will see that users want a general way to query over data, and create something like LINQ.
Of course, you're right that language designers shouldn't go for a 100% solution. Monads are kind of the classic 100% solution. You can do anything with monads, but that means you can do anything with monads.
That said, I think Rust does an impressive job at squeezing efficiency out of these tradeoffs. Sure, it trades off some developer productivity for extreme performance and safety, but its developer productivity story is still markedly better than other systems languages (and probably on par with some of the more cumbersome managed languages). Similarly, the tooling story is pretty great while every other systems language has pretty awful tooling (especially build systems). Moreover, Rust is getting better at a remarkable pace. I don't think it will ever close some of these gaps, but I think it will get close enough to pose a real threat.
So, logically it follows: aim for less than 80%. You can always add things; you can't take things away.
No we haven't; we've established (for very dubious values of "established") that aiming for more than 80% is frequently not worth the trouble.
One of the main usage of `Range` is to be an iterator.
The other is to causally slice data structures.
For both use-cases would the proposed changes lead to major usability regressions and braking. Because you can't compiler time enforce valid ranges as many are not created at compiler time (e.g. `a.start()..b.mid()`) and making range creation fallible would in practice be a massive usability nightmare. E.g. consider `for x in (start..end).unwrap().iter { .. }` instead of `for x in start..end { .. }`.
The current solution while imperfect was chosen to fit the most common use-cases of it best.
For some very performance sensitive use cases where you need slicing of ranges and the way the std range does thinks is to slow/bad you can have alternatives which are faster but have usability drawbacks. But that's the exceptional case not the normal case.
Many of the other examples shown also seem kinda strange. E.g. `get_unchecked` as well defined as "an out of bounds array index" is well defined (it's only defined for Range<usize> it's also an unstable experimental API...).
Range need clones => only in use-cases it was not primary designed for.
Range is unsure when its valid => No it knows it's always valid but not all valid ranges can be used in all places without having errors, indexing a slice with a range can panic anyway (out of bounds access) so moving the error handling there is fairly sane. Also you really can't have fallible Range creation.
Range hides a foot gune => any exclusive from-to range in any language has this problem, it's why in mathematics there are 4 types of ranges
A Recipe for Rearranging Range => he/she somehow assumes you can magically make sure that start <= end without error handling but without that oversight on his/here part this changes would make Range a usability nightmare.
Yes, I think it was a rushed type which should be a lot more constrained on 1.0 to allow for modifications later, which is how stuff is usually done in Rust. It also probably should have implemented IntoIterator instead of Iterator directly. It may also be my least favorite Rust type, but I don't think it's really that bad, and I'm thankful for whoever designed it as I find it still much better than slice-specific indexing syntax.
Note also that the clone issue and the borrow issue are not applicable to Haskell, and that the performance characteristics of Range may be hard to replicate while implementing Foldable or Traversable.
> Note also that the clone issue and the borrow issue are not applicable to Haskell
No, but Rust switching to a typeclass-based iterator syntax should help with this too.
> the performance characteristics of Range may be hard to replicate while implementing Foldable or Traversable.
I don't see why - rustc generally does (and must do) a great job of specializing parametric code.
They start off with a name that obviously will never be needed (eg "moz-blur") and work with that for a while until it becomes apparent what "blur" should be.
If the rust developers had named "Range" as "RustRange" or something else weird to start with, then they could come back through later and name it to the more desirable name. This seems like a good tactic whenever you're still trying to figure something out but intend to put it in production anyways.
fn main() {
let r = 0u64 .. 1;
let n = r.len();
| ^^^ method not found in std::ops::Range<u64>
println!("Hello, world! {}", n);
}
https://play.rust-lang.org/?version=stable&mode=debug&editio....len() is not the length of a slice induced by the range or the distance between start and end but instead it's the len method form `ExactSizedIterator`, which in turn is a "special case" of where `Iterator.size_hint()` is known to return a correct value.
The thing is `Iterator.size_hint()` does return a size, which is usize.
So `Range<u64>` can only implement `ExactSizedIterator` on 64-bit targets, which I guess is why someone decided that it's better to not implement it (at all) to not hinder portability of libraries as it would be quite easy to accidentally write a lib not working on 32 bit. Not sure if that is the right decision tbh.
I've been considering upstreaming a trait into the read_write_at crate to provide std::io::Result<u64> lengths for Mutex<impl std::io::Seek> / std::fs::File (on platforms where length is available without mutating seek position - such as on windows via file.metadata().map(|m| m.file_size()) per:)
https://doc.rust-lang.org/std/fs/struct.File.html#method.met...
https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataEx...
Context: multithreaded reads of zip archives & https://github.com/vi/read_write_at/issues/1 & https://github.com/MaulingMonkey/vfs-zip
There's a whole slew of u64 offsets and sizes... the occasional subtraction to calculate a maybe-larger-than-memory size doesn't seem like that big a deal. Occasionally there are methods implemented for it - typically named "file_size()" instead of "len()" though.
In some Rust forums, the language designers were shocked to learn that this is possible.
At any rate, the Range type was very clearly designed for slices of in-memory contiguous arrays. Then template magic was used to make them "generic over all types". So now we have a situation where Range acts like an array subset and a B-Tree range selector, and a source for ordinals in iteration, and a bunch of other things. Some of which are incompatible.
Who? Where? I guarantee you that the people responsible for the `usize` type, or its usage in the stdlib collections, were not shocked.
That iterators and ranges have this 1% edge case where you'll need to roll your own trait if you want "(0u64..1).len()" to compile because ExactSizedIterator was designed for the 99% case of in-memory collections, could be argued as a design mistake - or could be argued as a reasonable avoidance of overcomplication in std in favor of allowing the end users who encounter that edge case to solve the problem how they please, if it is indeed a problem for them.
trait Len64 {
fn len(&self) -> u64;
}
impl Len64 for std::ops::Range<u64> {
fn len(&self) -> u64 { self.end - self.start }
}
fn main() {
println!("{}", (0u32..!0).len());
println!("{}", (0u64..!0).len());
}
https://play.rust-lang.org/?version=stable&mode=debug&editio... 4294967295
18446744073709551615
Meanwhile, I'm still inheriting C/C++ codebases using APIs that know they're dealing with files and still use pointer-sized integers. In their defense, the system APIs they use often predate widespread 64-bit integer support in 32-bit C compilers (I'm looking at you, fseek/ftell). Less in their defense, the wrappers around said system APIs often postdate the very same, and postdate a slew of alternative APIs that don't even need 64-bit integer support.This is the key part. You really shouldn't try to "censor" the math to "help" users. It just causes more pain.
I'm recently been annoyed with the push back against "DynSized" again, which IMO is a symptom of the same thing. "DynSized" is the natural way to generalize Rust's notions of size/layout so we can implement custom DSTs, but people are scared of another ?Trait making Rust "more difficult" or "more complicated", so we're going to get some crapped up epicycles plan if we get anything at all.
-------
Incidentally the Jonothan blow quote below is full 180 misapplied. The issue is people trying to dial back something, not push it to it's natural conclusion, because they don't like the natural conclusion. Too bad, don't do that.
Modern languages and Rust specifically address that problem by letting you write clean programs with the illusion of immutability but still basically mutating state all over the place in the actual executable.
And that's if you care that much about speed. Even slow Rust is pretty fast, and the readability/consistency/maintainability benefit of RAII is immense compared to the tiny speed gains.
- Float ranges don't implement Iterator
- Rust is clever enough to make is_empty checks NaN robust, so `is_empty` return true
- contains checks if it's inside the stard bound and inside the end bounds, but whichever of the bounds is NaN is always not inside the bounds and as such the range won't contain anything (which matches with the behaviour of is_empty)
So it works well.
But float ranges are kinda useless for anything but the contains/is_empty methods.
You don't notice as much normally as most structs do not expose their internals and the constructors are often constrained.
No of the problems come from this not being constrained on PartialOrd. Instead they are related to other things like the usability benefit of range construction not being fallible (which is why we can't have a start <= end constraint in the type).
Some things could be done differently by constraining the type to always be an integer, but expect `constains` now not needing a reference non of the thinks the article complained about would be different just by constraining it to an integer. In practice many of it's method are anyway only defined for some Range types like you can only use `Range<usize>` for slicing.
The reason why constraints on structs isn't a good API design is because it limits future API design scope unnecessarily. In this case, maybe you want to reuse the Range syntax as part of a DSL?
>If you try to enforce that start <= end, you lose the ability to make a Range of non-comparable things
What even is a "range of non-comparable things"? Doesn't the very definition, included in the article, imply an ordering because of the "upper" and "lower" bounds? What on earth is a situation where "upper" is not necessarily greater than "lower"?
I mean consider:
`slice.get(start..end)`
against:
`(start..end).and_then(|range| slice.get(range))`
slice[start.end]
particularly because `[]` indexing is already a panicky operation.I.e. slice.get(5..0) == None
But unlike the other complaints this one is still fixable, with zero backwards incompatibilities.
All we'd need is someone to implement that lint (in, say, clippy). What it'd need to do is look for `IntoIterator::into_iter(x)` calls expanded from `for` loops, where `x` is a variable of a `Copy` type. And maybe look for mentions of the same `x` after the loop.
EDIT: left a comment on the GitHub thread, I might be misremembering https://github.com/rust-lang/rfcs/issues/2848#issuecomment-6....
The best thing I can come up with is that such a range defines a path from one tip of a (geomotric) vector to another vector and because vec in rust can have (theoretically) usize elements it's in a usize::MAX dimensional space ;=)
Going to the Step docs[1] you'll see that its only implemented by integer types and char. If Vec implemented Step, you could iterate over it.
Also, if you're referring to the "contains" guessing puzzle, you have to look at Range::contains docs to see it uses Vec's PartialOrd implementation (basically comparison), in which docs state its elements are compared lexicographically.
Usually in Rust the first thing I'll do when trying to do something very specific (iterate over a Range of Vecs) is try it at the playground (play.rust-lang.org) and/or just look at std docs.
[0] https://doc.rust-lang.org/std/ops/struct.Range.html#impl-Ite... [1] https://doc.rust-lang.org/std/iter/trait.Step.html#implement...
Let's say I have the range [1].. [2], what is in this range?
Well vectors are sorted in lexicographic order, so every vector that starts with 1.
What comes after [1]?
Suppose it's any vector whose first non zero digit is in place n (e.g. n = 2 and the vector is [1, 0, 3, 1]). I can make a vector that comes earlier in the ordering by just inserting another 0 (e.g. [1, 0, 0, 3, 1]). This means that the vector has no non zero digit... but it is obvious that [1] doesn't come after [1], and that [1, 1] comes before [2] so [2] isn't the next vector, so (by proof by contradiction) there actually just isn't a well defined concept of next vector. As a result I definitely can't just list the vectors in a range in order.
I think there is a well-defined next vector, it just isn't very useful. The next vector after [1] for a vector of unsigned integers would have to be [1, 0]. And then [1, 0, 0], [1, 0, 0, 0], etc. (For signed integers substitute T::MIN instead of 0.)
In rust the (useful) trait implementations on `Range<T>` only exist for `T: Step`.
Sure you can create other `Range<T>`'s but you can't use them for anything useful. (Ok, if they are PartialOrd you still can use `contains` and `is_empty`).
The article makes it look like a lot of problems comes from rust being too generic over the range type but that's not the case (expect for contains requiring a reference, but then BigNum's are a thing too).
I.e. the only reason why you can create a `Range<Vec<Mutex<Rc<String>>>>` is because it doesn't hurt anyone to allow me to do so. But I won't be able to use that rang for anything. It won't implement `is_empty`/`contains` nor will it be Iterable. Heck it doesn't even implement `Clone`. But non of the implementations around iterability, is_empty, contains etc. get in practice any problem because of this type being valid.
Instead the mentioned problems come mainly from:
- start <= end not being enforceable at type level as a fallible range constructor would have a horrible UX.
- Range<usize> if used for indexing treating things like 5..0 as "out of bounds" while for iterating it's just an "empty" sequence (which is not nice but a very reasonable decisions generally improving usability in practice due to how the range types are normally used in rust but confusing for some less common use case).
- The person somehow being hung up on the exact definition of "out of bounds" not being clear enough defined in the function documentation of a experimental nightly API which is perma-unstable, i.e. we are basically speaking about internal (but visible) implementation details of the standard library...
pub struct BTreeMap<K, V> {
root: Option<node::Root<K, V>>,
length: usize,
}
The two type parameters are unbounded, this gives flexibility in the implementation because it allows you to define separate `impl` blocks where the bounds are different and more specific to their use case.Have a look, https://doc.rust-lang.org/src/alloc/collections/btree/map.rs...
Sometimes the bounds are just Clone, other times they are more complicated Ord bounds for functions like `get`.
This is the way Rust libraries are written, it's very flexible and it's the right choice IMO.
E.g. Iterator is only implemented for things implementing the currently unstable Step trait which for now are mainly integers and char.
E.g. only Range<usize> can be used to index/slice things.
I'm not a seasoned rust programmer though, is there a better, more explicit way to do what I attempted there?
A .step_by() that accepts negative values a la python would be nice, but I can live without.
I'd love a REPL for this sort of thing though - a website just isn't as good.
The size_hint is implemented here [2]. This is where the "start < end" check happens.
[1] https://doc.rust-lang.org/src/core/iter/traits/exact_size.rs...
[2] https://doc.rust-lang.org/src/core/iter/range.rs.html#500-55...
len() is hidden in ExactSizeIterator, which is implemented for a bunch of Range specializations: https://doc.rust-lang.org/std/ops/struct.Range.html#impl-Exa...
Expanding the docs node and following the [src] link for len, we can see it's using ExactSizeIterator's default implementation, which invokes size_hint: https://doc.rust-lang.org/src/core/iter/traits/exact_size.rs...
ExactSizeIterator only requires Iterator: https://doc.rust-lang.org/src/core/iter/traits/exact_size.rs...
And sure enough, that's where size_hint is implemented in Range: https://doc.rust-lang.org/std/ops/struct.Range.html#impl-Ite... -> https://doc.rust-lang.org/src/core/iter/range.rs.html#515-52...
Which in turn relies on steps_between, which is implemented via macro for various integer types:
https://doc.rust-lang.org/src/core/iter/range.rs.html#240-24...
https://doc.rust-lang.org/src/core/iter/range.rs.html#360-37...
https://doc.rust-lang.org/src/core/iter/range.rs.html#388-40...
It's from `ExactSizedIterator.len()` and is a specialization of the case where `Iterator.size_hint()` returns known to be correct values.
It's a bit confusing on `Range<>` as it's not the distance between start/end or anything like that (well it's the number of steps when iterating Range so kinda the distance).
It also has the side-effect that it's not implemented for Range<u64> as I guess not to hinder 32bit compatibility (len/size_hint return a usize).
Because it's the iteration length it also is calculated based on iterations criteria, simplified `if start < end { end- start } else { 0 }`. Except in practice it goes from `len` to `size_hint` to the `Steps.steps_between` API in-lining all the calls and optimizing away the unnecessary overhead.
Rust has comparison traits... why aren't those involved here? It seems like it would be straightforward to ensure that any Range's start and end can only be Ord's, and that the first value must be < the second.
> Range<Vec> requires the borrow, so the vastly more common Range<usize> etc. forces it as well.
Not sure why this has to be the case. Why the implicit reference? Why not, when you want to use references in your range (a fairly exotic usecase), you have to do so explicitly?
&vec1..&vec2(I can also think of situations where you'd pass around strings as vectors of characters, but I think that's a bit of a stretch.)
Ironically though, that should be actually more objectionable than ordering arrays by lexicographical order of elements, because the underlying code point order is pretty much useless for any other purpose than having some arbitrary deterministic order. Meaningfully ordering strings is an inherently locale-dependent operation, and PartialOrd has no way to take this into account.
Lexicographic order is the most common choice for vector; the other reasonable choice is length-then-lexicographic.
Except that it's well defined: If you start or end after then end of the array/slice/vector. The same way it's done for any other array/slice/vector access.
Could the documentation of this nightly experimental API be better, sure. Is it undefined or unusable? No it's as well defined as "an array index being out of bounds".
Oh and it's only defined for Range<usize> before you wonder there is no confusion with any ranges of custom types, this a method of a trait implemented for only for Range<usize> which generalizes the indexing of slices and of which everything but the name is currently unstable/experimental.
The docs about these feel written by someone who knew that "some API like this" would be a good idea, but that somehow never managed to flesh out what these traits should semantically imply.
Which is kind of dumb, given that there was _excellent_ prior art about this when Rust was created (Elements of Programming, From mathematics to generic programming, the C++ standard library and the dozen papers about operator<=>, ...).
And that's one of the things I dislike more about rust. The way to overload operators, like +, or <, uses trait names, like Add or PartialOrd, which suggest that these operators have certain semantics (particularly when using Add in where clauses), but in practice they lack any semantic meaning and are just syntactic things.
Which is why, e.g., the standard library implements "Add" for strings. That doesn't mean that it implements "Addition" for strings, but rather that it overloads the Plus operator. And in the String case it does so to implement "Concatenation".
Which is IMO super dumb, because they could have just fixed this by naming the `Add` trait `Plus` instead, which is what languages that do the same thing, like C++, already do (`std::plus`, `operator+`, ....).
You could argue that using `+` to implement concatenation is "bad", but it is way less worse than using "Addition" to implement concatenation, which is what Rust ends up requiring everybody to do because that's just how you overload the `+` operation.
They are binary relations, just some that are more "niche" than the more common ones. PartialEq actually has a link to it's mathematical definition on the docs [0].
[0]:https://en.wikipedia.org/wiki/Partial_equivalence_relation
Thanks for making my point: this API provides _a_ binary relation is IMO useless. There are millions of binary relations that one could implement for a type, and often many that make sense implementing for a particular type, and that this API doesn't support (e.g. there are both strict partial order and total orders for float in the IEEE standard; this API however implements none).
For this to be useful, the docs would at least need to say what can one assume about the partial order implemented by PartialOrd (is it strict? is it non-strict? something else?), and ideally have a solid ordering hierarchy so that APIs and algorithms can pick what makes sense to them, instead of having to assume the lowest-possible denominator imaginable, which results in, e.g., it not making sense to implement ordering for floats in the standard library, even though to be IEEE compliant it would actually need to do that.
Is this indeed so? Presumably on an empty slice, but then `get` would just return the empty slice again?
Having run into the "reverse range does not contain what you obviously expect it to contain" before and wasting a few hours on it, like many other people have and will continue to do in the future, definitely makes me want to call it a wart.
`.contains()` is only implemented for `Range<Idx: PartialOrd>`, which to me implies that when checking whether a value is contained in the Range, it has enough knowledge about the ordering of numbers that it should be able to still do a bounds check on reversely ordered numbers.
So I have no idea about rust, but in c++ a big feature of templated code is that you can make type appropriate specializations, is that not possible in Rust or did its standard library maintainers just sleep on the job?
Last I checked (a month or two ago), the specialization feature warning even included a bit about how it can crash the compiler (the crashes were the reason I gave up on specialization two years ago)
Edit: Turns out theres been some progress since I used it last! Feature `min_specialization` seems to not be a non-crashing subset of specialization [1]
But it would also be uncommon in C++ to specialize to pass by value. Generic code normally just passes T by reference, ex. max, push_back. Since this is transparent to the caller in C++ though, you don't have to write the &s.