Slice range notation format

Preferred slice range notation format

  • Allow specifying a start and an end index.

  • For each start/end index: choose inclusive or exclusive.

    Only shrink-only range logic is allowed:

    • No prefix → inclusive

    • Prefix ! → exclusive

  • Omitted start or end defaults to the original array’s actual start or end.

Examples

  • [1..4] → slice from index 1 to index 4 (both inclusive).

  • [!1 .. !4] → slice from index 2 to index 3 (exclude 1, exclude 4).

  • [..] → entire array, from index 0 to the last index.

  • [! .. !] → slice from index 1 to max_index - 1 (exclude original first and original last element).

  • All other combinations can be freely composed.

Rationale

  • The rule is explicit and requires no arbitrary memorization.

  • It follows natural human reading intuition.

Unwanted slice range notation format

  • Dislike Python-style [1..3] design: left-inclusive but right-exclusive, which is counter-intuitive for human reasoning.

As for the choice of the prefix symbol, I think it can still be further discussed and refined.

I think using < for exclusive and = for inclusive is a lot more intuitive.

  • inclusive-inclusive: sliceable[start=..=end],
  • inclusive-exclusive: sliceable[start=..<end],
  • exclusive-inclusive: sliceable[start<..=end],
  • exclusive-exclusive: sliceable[start<..<end].

That being said, for slicing, inclusive-exclusive is vastly more useful than most, and I find it quite intuitive. I don’t even remember encountering a case were I needed an exclusive start.

8 Likes

Out of the 4 possibilities, I’d argue it’s the most intuitive to work with.
e.g. it has the property that end - start gives you the length, which is something that people mess up in real life when using other systems (e.g. to calculate the number of days given inclusive start and end dates, many people resort to counting them on their calendar)
The only other system that has this property is left exclusive right inclusive, but that really makes no sense, since it would require -1 to slice things at 0.

It would add a new symbol to memorize though, and it’s also easy to miss when reading, and easy to forget when writing.

5 Likes

There is already a standard notation for inclusive/exclusive ranges. The downside is that conventional coding editors assume balanced brackets and the standard notation mixes them. We just need better editors.

Then you had to write for (0..!foo.len) |n| .. which I don’t find very intuitive.

if need to traverse all, just using this is OK;

The current state is to just do something like that;

const my_slice: []u8 = foo();
for (my_slice) |element| {
    // use element
}

Why would [..] be needed?

In general I’m unsure about what and if anything you’re trying to achieve with this. The [inclusive..exclusive) range syntax and specification is common and widely used and understood in nearly all programming languages for reasons already explained above. The only major exception I can think about off the top of my head is Julia, which has inclusive bounds for both lower and upper.

It would be nice to have ..= in Zig, but of course one can do without it.

3 Likes

It is the other way around. The [inclusive, exclusive) range syntax is what mathematicians use.

6 Likes

And this would be fine syntax if not for dumb editors.

… people who forget which one is inclusive and which one is exclusive. It is an obscure math notation that is not that common in programing languages.

foot(gun[42..69);

Can you fix the typo here? Did I miss ] or )?

2 Likes

for-loop ranges can be inconvenient.
I just had one again.
Imagine two u6 vars a and b with which you want to compute something inclusive.
In the loops there is a useless overflow danger, while we do not need the + 1 value.

fn calc(a: u6, b: u6) void {
     if (a > b) {
        for (b..a + 1) |v| {
        }
     }
    else if (a < b) {
        for (a..b + 1) |v| {
        }
    }
}
1 Like

If you use something like this:

fn calc(a: u6, b: u6) void {
    const l:u7, const u:u7 = if (a < b) .{a, b} else .{b, a};
    for (l..u + 1) |v| {
    }
}

There is no overflow danger because currently in zig v is a usize.
But I would also like if we could have typed smaller indices like this:

1 Like

yes… easily solvable when we typecast. easily solvable anyway but sometimes inconvenient.
Typed looping vars would be great indeed.

1 Like

Adding to this somewhat old thread..
I personally am generally fond of clearer indications of inclusive and exclusive ranges like ..< and ..=.

Something that could also be nice in the realm of slice syntax would be the ability to specify start + length. Something like [start..+10]. Thought of this while looking through this thread: Reading integers from dynamic address - #2 by dbushell

Something else that is related to slicing/ranges, which I know from VHDL, but might also be available in Ada (or others), is the ability to use ranged integer types as slices/ranges, which would require #3806.

As an example from VHDL where a new integer subtype with a range from 0 to 255 is declared, and used to specify the range / size of an array type:

subtype ram_addr_t is natural range 0 to 255;
type ram_t is array(ram_addr_t) of word_t;
signal ram : ram_t;

The integer type can also be used to take a slice from an array:

subtype opcode_range_t is natural 31 downto 26;
subtype instruction_t is bit_vector(31 downto 0);
signal instruction : instruction_t;
-- Later ...
opcode := instruction(opcode_range_t); -- ( ) are used to index into arrays

Why shouldn’t we just copy Odin with ..< and ..=?
These at least look somewhat intuitive with their meaning.

4 Likes

I have heard it said that programming never has any new ideas, just old ones and a while() loop.

Remark The programming language Mesa, developed at Xerox PARC, has special notations for intervals of integers in all four conventions. Extensive experience with Mesa has shown that the use of the other three conventions has been a constant source of clumsiness and mistakes, and on account of that experience Mesa programmers are now strongly advised not to use the latter three available features. I mention this experimental evidence —for what it is worth— because some people feel uncomfortable with conclusions that have not been confirmed in practice. (End of Remark.)

In modern terms: it’s a footgun.

Only thing I really want is x[2..-1] I’m fine if it only works with numeric literals.

I used to think both of Odin’s representations were pretty good. But after thinking about it a bit more, now I hope that no matter which one is used, there’s at least only one range syntax.

I agree that both the left-closed right-open and left-closed right-closed approaches have their advantages, but they also have their own pitfalls. In my view, their pros and cons almost always exist, so choosing one basically means accepting its problems for the sake of its benefits. That’s why I think mixing them is always worse, because then you constantly have to be aware of which one you’re using and watch out for the potential footgun of each approach.

The advantages of left-closed right-open intervals are clearly explained by Dijkstra, but their drawbacks are also obvious. If you want to represent a range of n-digit numbers, you have to introduce n+1-digit numbers. Otherwise, you might run into overflow footguns.

Zig’s approach is to require that all ranges must be usize. Obviously, this isn’t the most pleasant approach, as it forces you to use a lot of annoying @intCast and you can’t use negative numbers. But after paying that price, we get left-closed, right-open range that feel intuitive to programmers, along with all the advantages that Dijkstra praised. Since usize is huge enough that you basically can’t traverse it completely, this hacky method effectively avoids overflow risks.

Using closed intervals on both ends avoids overflow issues, but it sacrifices all the benefits that Dijkstra praised and goes against programmer intuition. Almost all modern programming languages use left-closed, right-open intervals, which has become the norm, and going against it will definitely cause more bugs.

But anyway, after thinking it over, I still believe there should only be one way to represent a range, no matter what flaws it has.

Maybe there’s a win-win solution: using the left-closed, right-open approach, while also being able to automatically handle overflow issues. Hopefully #3806 can make this happen.