RangeFrom, Part 2..: What I think is wrong about the design

13 points by valdemar 12 hours ago on lobsters | 12 comments

ettolrach | 10 hours ago

This means that it is not always monotonically increasing since it will stay the same.

To me, the order relation used for monotonicity is usually less than or equal <=, not strictly less than <. In that view, the function is monotonically increasing, since 255 <= 255.

jackdk | 3 hours ago

I was always taught that "monotonically increasing" meant x < y implies f(x) < f(y), and the property you describe was called "monotonically nondecreasing".

[OP] valdemar | 9 hours ago

I guess I should have written "strict monotonic" or something to make it clear what I meant. I was looking up the definition when I was writing it and somehow still messed it up. (I might fix that up tomorrow and link back to here)

juliaaa | 5 hours ago

This is pedantically true, but I think I mostly would interpret monotonically increasing as the strict form, especially in a non-mathematically context.

madsmtm | 11 hours ago

Hmm, after reading this I'm kinda liking the "overflow in release to the start" option? And have that be the behaviour for all of the types in the standard library?

So your table here would just list "Debug: panic!, Release: Overflow to start"

madsmtm | 11 hours ago

I think the primary argument for panicking is that it makes for a more resilient system? If you've specified that you want an unbounded range, then having it reach the end of those bounds is an error condition IMO.

I guess that's also an argument for returning None? But then again, the more resilient option might be to always panic on overflow? Unlike with integer addition, I'd suspect there isn't much of a performance benefit to not having a branch?

In any case, we can agree that NonZero<u*> saturating is insane.

Relatedly, one of matklad's blog posts a while back also argues that infinite loops are a bad idea.

juliaaa | 5 hours ago

Sort of unrelated, but I've been convinced by a colleague that Range types should default to being inclusive by default.

The primary counter-example being how do you represent ranges that include the uXX::MAX value? This is exactly the issue in this post.

For instance, for i in 0..=255u8 { } is necessary if you want the value to be a u8. With an exclusive range you need an extra bit to be able to represent the full range, since ExclRange { start: 0, end: u64::MAX } doesn't cover the whole range, and ExclRange { start: 0, end: u64::MAX + 1 } obviously doesn't work (and if it did would complicate any kinds of contains() check with edgecades). As far as I know the latter is not possible to represent in Rust's standard Range type since Range { start: 0, end: 0 } is treated as empty.

This is also not documented anywhere in Rust's stdlib documentation about this behaviour and what happens right at the end here. Say you have 0xf0u8 and a size of 0x10, then you'd expect the range Range<u8> { start: 0xf0, end: 0xf0 + 0x10 } to work, but it will instead silently be empty. Whoops.

Inclusive ranges sidestep this problem entirely.

quasi_qua_quasi | 5 hours ago

If you have inclusive ranges and wrapping behavior, how do you represent an empty range?

juliaaa | 5 hours ago

You don't.

Specifically I'm referring to ranges where you don't support wrapping around the end. i.e. end < start is treated as invalid. If you're in exclusive land, that's end <= start as invalid condition.

Any of the invalid ranges can be treated as "empty" if you so desire. I would argue that you usually never want an empty range, but an invalid one that contains no values.

If you support wrapping around the end, you can't have an empty range in either the inclusive or exclusive representations. Exclusive gathers that empty range in the representation Rust uses by assigning (many) valid ranges as invalid.

quasi_qua_quasi | 2 hours ago

Ah okay I see what you mean, got it.

juliaaa | 4 hours ago

Alternative representation is: (start, size)

Either size=0 is empty, in which case you can't represent a range covering the full integer size, or size=0 means uXX::MAX + 1, in which case you can represent the full size but not the empty size.

Rust's exclusive representation chooses the former. In theory you could choose the latter as well, but it adds more checks to all the code: no longer can you simply check start <= end as "valid".

An inclusive representation lends itself to the latter interpretation, and allows you to easily use a single condition for validity.

Inclusive maps more naturally unto the integers, and thus how one expects numbers to behave if we forget about overflow.