Skip to content

Consider switching to numerical indexing for Math structures #34

Description

@Cheatoid

1. Performance and memory layout

  • Array part vs hash part:
    Lua tables have an array part (for integer keys 1..n) and a hash part (for everything else, like "X", "Y", "Z").
    Numerical indexing lets Lua store your components in the array part, which is:

    • Faster to access (no hashing, no string lookup)
    • More compact in memory
  • Tight loops:
    Math-heavy code (physics, rendering, linear algebra) often runs in tight loops.
    Doing:

    v[1] = v[1] + dx

    is cheaper than:

    v.X = v.X + dx

    especially when repeated thousands of times per frame.


2. Generic algorithms become trivial

Numerical indexing makes math code dimension-agnostic:

local function add(a, b, out)
    out = out or {}
    for i = 1, #a do
        out[i] = a[i] + b[i]
    end
    return out
end

That works for:

  • Vector2D (size 2)
  • Vector (size 3)
  • Vector4D (size 4)
  • Arbitrary-length vectors

Whereas if you use named fields (X, Y, Z), you either:

  • Write separate functions for each dimension, or
  • Hardcode field names, which doesn’t scale.

Same for matrices:

for i = 1, rows do
    for j = 1, cols do
        c[i][j] = a[i][j] + b[i][j]
    end
end

That’s only natural with numeric indices.


3. Interop with array-based APIs

When you need to...

  • Serialize to binary formats
  • Talk to C/C++ via FFI
  • Pass data to shaders or engines that expect flat arrays

…then having your vectors/matrices as numeric arrays is a direct fit:

-- Vector as {X, Y, Z}
buffer[1] = v[1]
buffer[2] = v[2]
buffer[3] = v[3]

No mapping from X, Y, Z to indices every time. It's nicer to Lua binary module authors...


4. Cleaner metatable tricks

You can have the internal representation be numeric, but still expose X, Y, Z for ergonomics:

local Vector = {}
local Vector_mt = {}

function Vector.new(x, y, z)
    return setmetatable({ x, y, z }, Vector_mt)
end

setmetatable(Vector, {
    __call = function(cls, ...)
        return cls.new(...)
    end
})

function Vector_mt.__index(t, k)
    -- This or use lookup/dispatch table
    if     k == "X" then return rawget(t, 1)
    elseif k == "Y" then return rawget(t, 2)
    elseif k == "Z" then return rawget(t, 3)
    else return rawget(Vector_mt, k) end
end

function Vector_mt.__newindex(t, k, v)
    -- This or use lookup/dispatch table
    if     k == "X" then rawset(t, 1, v)
    elseif k == "Y" then rawset(t, 2, v)
    elseif k == "Z" then rawset(t, 3, v)
    else rawset(t, k, v) end
end

return Vector

This gives:

  • Fast numeric storage (t[1], t[2], t[3])
  • Nice field access (t.X, t.Y, t.Z)
  • Shorthand constructor (Vector(1, 2, 3) instead of Vector.new(1, 2, 3))

Best of both worlds. 😎


5. Consistency with math libraries and conventions

Most math libraries (in Lua and other languages) treat vectors/matrices as arrays:

  • v[1], v[2], v[3]
  • m[row][col]

Using numeric indexing:

  • Makes code feel familiar to anyone with linear algebra background
  • Plays nicely with generic utilities (map, reduce, #v, etc.)

6. Easier dimension changes and refactors

If you start with Vector and later need Vector4D (e.g., homogeneous coordinates), numeric indexing makes it a non-event:

  • Algorithms that loop 1..#v just keep working.
  • No need to introduce a new field name (W) everywhere.

TL;DR

Using numeric indexing for Vector/matrices:

  • Leverages Lua’s array optimization (speed + memory)
  • Enables generic math code that works for any dimension
  • Fits better with FFI/binary/shader-style APIs
  • Still allows ergonomic v.X/v.Y/v.Z via metatables if you want

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions