Fantastic to see this being discussed. As it happens, I have been considering the general idea myself off and on for the last few years, and last year I did a little work on a drop-in replacement for range to add a bunch of extra functionality, such as representing infinite and semi-infinite ranges.
My question is: why do they need to be separate? Maybe there’s some advanced Numpy thing I’m neglecting, but I feel like all the differences are reconcilable.
rangehas to have definite endpoints - why? There’s nothing conceptually wrong with the idea of a semi-infinite range (granted, it would be hard to define iteration order for infinite ranges, and hard to specify e.g. “the set of all even numbers” vs. “the set of all odd numbers”). Nothing prevents starting at astartpoint and increasing bystepindefinitely; and nothing prevents an O(1) calculation of whether a given integer would beinthat sequence.rangedoesn’t have anindicesmethod - why not?'foobar'[range(3)]makes about as much sense to me as'foobar'[slice(3)]does.sliceisn’t iterable - why? It would be useful for a lot of user-defined types to be able to define__getitem__on a slice by, say, using a list comprehension to iterate over the indices that are conceptuallyinthe slice (after normalizing endpoints). I assume it’s because thestartandstopcan be arbitrary objects, but just how much do we gain that way? Integers andNoneseem to be by far the common case, even with Numpy. The other fancy Numpy trick is masking, but that uses a completely different type for subscripting.- The
reprs work differently - seems pretty trivial.
Of course, I suppose I am proposing breaking changes, and I get that the idea of 4.x is still roundly rejected. But I think the breaks are in pretty obscure cases, and perfect semver is a pipe dream anyway.
My original idea was: unify these types, and continue using the : syntax - including surrounding square brackets for clarity (and to avoid any precedence issues). With the ability to represent semi-infinite ranges, this neatly replaces both itertools.count, and enumerate:
# old
for i, element in enumerate(sequence, start):
...
# I never liked how the `enumerate` arguments are in the opposite order of the unpacked values.
# new
for i, element in zip([start:], sequence):
...
Better yet, this would automatically allow adding a step value, which would be nice to have .e.g in game engines:
# draw the map, tile by tile, at a specific location on screen
for x, row in zip([x_offset::dx], level):
for y, cell in zip([y_offset::dy], row):
render(cell, x, y)
This avenue also seems promising, if distinctions do need to be made. Conceptually, slicing the set of all integers ought to give the corresponding range, yes? So we just need a builtin object to represent that.
This seems pretty wild. Dunder methods aren’t supposed to be called directly, so I assume you have in mind that the range (or slice, depending on other decisions) literal syntax would wrap a call to __range__? That leaves the question of choosing which class to check for a method (I imagine that using the NotImplemented protocol, but three ways… oof). The benefit seems questionable, anyway; string ranges in particular seem problematic (what will you do when there’s more than a single character in the endpoints? What if the endpoints surround the surrogate-pair Unicode range? What if the stop is left unspecified?).