Built-In Support for Array Slicing and Negative Indexing

Greetings,

As a Roblox developer, it is currently impossible to slice tables and use negative indexing natively. Currently, you’d have to develop some exotic function or use metamethods to mimick those methods in a strange way. My ListLib module uses __call to slice tables, which is syntactically strange, not to mention the function has to be user-defined.

Negative indexing starts with the last element, as opposed to positive indexing that starts with the first:

local array = {1, 2, 3, 4, 5, 6, 7}

print(array[1]) --1
print(array[-1]) --7
print(array[-2]) --6

Currently, negative indexing does not error, just returns nil, so this should definitely become a feature!

Slicing can be used to get chunks of the table via start, end, and step indices, which can be negative:

print(array[2:4]) -- {2, 3, 4}
print(array[2:-2]) -- {2, 3, 4, 5, 6}

-- missing index number defaults to start/end, depending on which is omitted
print(array[4:]) -- {4, 5, 6, 7}
print(array[:3]) -- {1, 2, 3}

-- with step (defaults to 1)
print(array[3::2]) -- {3, 5, 7}
print(array[::3]) -- {1, 4, 7}

-- negative steps go in reverse
print(array[::-1]) -- {7, 6, 5, 4, 3, 2, 1}

Of course, these integers can be variables.


Use Cases

Snip-Snip

There are many times where I need to snip of the first or last element of an array. Currently, it’d be an inconvenient two-line process:

local snipped = {table.unpack(array)} -- make a copy
table.remove(snipped, 1) -- remove first element

But then what if I want to snip off multiple elements? table.remove does not allow multiple indices to be given, which would result in either a for loop or multi-lined removal.

Last Element

There are also many instances where I want to get the last (or second to last, etc.) element of an array, which the only way right now is array[#array] which lengthens with the variable name. Of course, array[-1] is undeniably shorter.


Now, it’d be great to have the slicing syntax be with : similar to Python, but in any case where it may clash with Luau type checking (such as the “as” :: operator), a new function can be added to the table library:

table.slice(array: table, start: int = 1, end: int = -1, step: int = 1) -> table

If these were added, it’d improve my development experience because I’d be able to swiftly access elements as fit for the circumstance and snip tables in a single line. The table library, I find, is quite limited such as not allowing multiple indices to remove when removing, so any workarounds would be inconvenient. With Luau branching off from the original Lua 5.1 pipeline, it’s syntax has retained most aspects of traditional Lua, but with certain additions such type checking syntax and compound operations. Therefore, slicing and negative indexing would add to this collection of convenient features.

Thank you.

12 Likes

Currently, indexing with a negative number will get the value associated with that index (which is very different from just returning nil). Lua doesn’t have a special type for arrays, so using a table as an array will have the same semantics as if it was used for anything else. The problem with negative indexing is that it fundamentally changes the semantics for table accesses. Index -1 not being in the table means that t[-1] results in nil, changing that would be breaking. Assigning to index -1 sets index -1, changing that would be breaking. This would also break properties like rawequal(a,b)==false (where a and b are both not nil or NaN) meaning that a and b refer to different table indices.

There would also be cases where -1 could refer to multiple values.

local t = {[-1]=1,2} -- negative indexing shouldn't work in constructor
print(t[-1])
-- will this print 1 or 2
t[-1] = 3
-- assigns to index 1 or index -1?
-- might to index 1 with negative indexing
-- currently assigns to index -1

There are problematic ambiguities with this syntax.

print(t[a:b()])
-- would this call method on 'b' on 'a'
-- or use 'a' as lower bound and 'b()' as upper bound
-- currently does the former
print(t[a:b():c()])
-- another example of this ambiguity with 4 possible interpretations
-- print(t[(a:b():c())])
-- print(t[(a:b()):(c())])
-- print(t[(a):(b():c())])
-- print(t[(a):(b()):(c())])
print(t[a::number])
-- would this cast 'a' to number
-- or use 'number' as step (and use 'a' as lower bound)?
-- currently does the former

Currently, the functions in the table library don’t handle negative indices like what you want, negative values are simply handled as any other value. table.unpack(t,-2,1) gets the values from -2 to 1, not from the second last index to 1. Changing the semantics of the table library functions would be quite breaking. A new library function which handles negative indices differently would be quite inconsistent and unintuitive.

local t = {[-2]=1,[-1]=2,[0]=3,4}
print(table.unpack(t,-2,1)) --> 1 2 3 4
10 Likes