Learn Lua in Neovim: Tables as Arrays — 1-Based Indexing, Insert, Remove & Length | Episode 17
Video: Learn Lua in Neovim: Tables as Arrays — 1-Based Indexing, Insert, Remove & Length | Episode 17 by Taught by Celeste AI - AI Coding Coach
Lua Tables as Arrays: 1-Based Indexing, Insert, Remove
Tables are Lua's universal data structure. As arrays:
{ "a", "b", "c" }, indexed 1 through #t.table.insert,table.remove,#tfor length. The 1-based indexing is the surprise.
Tables are the data structure in Lua. Same construct used as arrays, dictionaries, structs, objects, and modules. Today we use them as plain arrays — sequences of values indexed by integers.
Creating an array
local fruits = { "apple", "banana", "cherry", "date" }
print("First fruit: " .. fruits[1])
print("Third fruit: " .. fruits[3])
print("How many fruits: " .. #fruits)
Output:
First fruit: apple
Third fruit: cherry
How many fruits: 4
The { ... } syntax creates a table literal. Items separated by commas. As an array, the first item is at index 1.
1-based indexing
Most languages use 0-based indexing — arr[0] is the first item. Lua uses 1-based:
local fruits = { "apple", "banana", "cherry" }
print(fruits[1]) -- "apple"
print(fruits[0]) -- nil (no zeroth item)
This is the single biggest surprise for programmers coming from C, Python, JavaScript, etc. There's no way around it — 1 is "first" in Lua.
The historical reason: Lua draws on Pascal and pre-C languages where 1-based was common. It also matches the way humans count.
# for length
print(#fruits) -- 4
The # operator returns the length — the count of items in the array part of the table.
It's defined for tables with no internal nil gaps. With gaps, # is undefined — could return any boundary:
local t = {1, 2, nil, 4, 5}
print(#t) -- 2, 5, or anywhere in between (implementation-defined)
For arrays you treat as sequences, never nil out an element you still want to count. To "remove" an item, use table.remove.
Insert and remove
table.insert(fruits, "elderberry")
print("After insert: " .. #fruits) -- 5
print("Last fruit: " .. fruits[#fruits]) -- "elderberry"
table.remove(fruits, 2)
print("After removing index 2: " .. #fruits) -- 4
table.insert(t, value) appends value to the end. Equivalent to t[#t + 1] = value.
table.insert(t, pos, value) inserts at a specific position, shifting later items down.
table.remove(t, pos) removes the item at pos, shifting later items up. Returns the removed value:
local removed = table.remove(fruits, 2)
print("Removed: " .. removed)
table.remove(t) (no position) removes the last item — like pop in other languages.
Iterating
for i = 1, #fruits do
print(i .. ". " .. fruits[i])
end
Standard for loop with #fruits as the upper bound. Reads top-to-bottom, in order.
There's also ipairs (covered in episode 20):
for i, v in ipairs(fruits) do
print(i .. ". " .. v)
end
ipairs walks the array part starting from index 1, stopping at the first nil. Same result for a normal array.
Setting and clearing slots
fruits[5] = "fig" -- assign
fruits[5] = nil -- "remove" by setting to nil
Setting to nil removes the slot. But this can leave a gap that breaks #:
local t = {1, 2, 3, 4, 5}
t[3] = nil
print(#t) -- could be 2 or 5
To shrink an array safely, use table.remove — it doesn't leave gaps.
Adding at any index
local t = {}
t[1] = "a"
t[2] = "b"
t[3] = "c"
Same as {"a", "b", "c"}. The literal is just sugar — under the hood, tables are flexible mappings from any key to any value.
t[100] = "far away"
print(t[100]) -- "far away"
print(#t) -- 3 (the array part stops at the first nil gap)
You can index any integer; the table holds the value. But # only counts the contiguous prefix.
Mixing array and dictionary
local t = {
"first",
"second",
name = "things",
}
Lua tables are flexible — you can mix array entries (indexed 1, 2) and named entries (string keys) in the same table. We dive into the dictionary side in episode 19.
Common stumbles
0-based thinking. t[0] is nil, not the first element. t[1] is first. Period.
Off-by-one in loops. for i = 0, #t - 1 skips the last element and reads t[0] (nil). Use for i = 1, #t.
Setting to nil to "remove." Leaves a gap; breaks #. Use table.remove.
Counting on # for tables with nils. Undefined. Track the count yourself or use select with variadics.
ipairs stopping at the first nil. That's the contract — ipairs walks 1, 2, 3 ... and stops when it hits a nil. For dictionary-style iteration (any key), use pairs (episode 20).
What's next
Episode 18: table array operations. table.sort, table.concat, custom sort comparators, building a small to-do list with insert(t, pos, v).
Recap
Tables are Lua's universal data structure; today we use them as arrays. 1-based indexing. { "a", "b" } literal syntax. #t for length (undefined with gaps). table.insert(t, v) to append, table.insert(t, pos, v) to insert, table.remove(t, pos) to remove and shift. Use table.remove, not t[i] = nil, when you want to shrink the sequence.
Next episode: sort, concat, and to-do lists.