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.
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.
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.
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.
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.
`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.
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.
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.
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.
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.