Multiple Indices

lua-users home
wiki

I recently needed some functionality that allowed multiple indices. Accessing it via with an array as "key" was pretty useful. It was purpose-built for my needs -- both its semantics and performance profile -- but maybe somebody else can get some use out of it or refine it. I wasn't sure if a true table could be used because it relies on consistent iteration order. I wasn't sure if tables offer that.

-- Creates a special table that accepts keys that are arrays..
-- You must specified the expected length of this array when
-- creating a MultiTable.
--
-- Given mt = MultiTable(3), accessing mt[{1,2,3}] semantically
-- acts like mt[1][2][3].
--
-- You can also access using a function call syntax.  The following
-- are equivalent:
--    mt[{1,2,3}], mt(1,2,3), mt{1,2,3}
--
function MultiDimTable( dimension )
    if not dimension then
        error( "MultiDimTable expected dimension, got nil" )
    end

    local mt = {}    -- all the nested access is hidden here

    local function mt_index(t, k)
        if type(k) ~= 'table' then
            return t[{k}]
        end
        if dimension ~= #k then
            error("MultiTable key dimension was " .. #k .. ",
                  expected " .. dimension)
        end
        local v = mt
        for i = 1, #k do
            v = v[ k[i] ]
            if not v then return nil end
        end
        return v
    end

    local function mt_newindex(t, k, v)
        if type(k) ~= 'table' then
            t[{k}] = v
            return
        end
        if dimension ~= #k then
            error("MultiTable key dimension was " .. #k .. ",
                  expected " .. dimension)
        end
        local n = mt
        for i = 1, #k - 1 do
            local n_old, k_i = n, k[i]
            n = n[k_i]
            if not n then n = {} ; n_old[k_i] = n end
        end
        n[k[#k]] = v
    end

    local function mt_call(t, k, ...)
        if type(k) == 'table' then
            return t[k]
        end
        return t[{k, ...}]
    end

    return setmetatable( {}, {
        __index = mt_index, __newindex = mt_newindex, __call = mt_call } )
end


-- tests
do
    local res, err
    res, err = pcall( function() MultiDimTable() end )
    local mt1 = MultiDimTable(1)
    mt1[4] = 5
    assert( mt1[{4}] == 5 )
    assert( mt1[4]   == 5 )
    assert( mt1{4}   == 5 )
    assert( mt1(4)   == 5 )

    mt1['abc'] = '123'
    assert( mt1[{'abc'}] == '123' )
    assert( mt1['abc'] == '123' )
    assert( mt1({'abc'}) == '123' )
    assert( mt1('abc') == '123' )

    res, err = pcall( function() return mt1[{1,2}] end )
    assert( not res and string.match(err,
        'MultiTable key dimension was 2, expected 1') )

    res, err = pcall( function() mt1[{1,2}] = 4 end )
    assert( not res and string.match(err,
        'MultiTable key dimension was 2, expected 1') )

    for i = 1, 100 do
        local n = {}
        for j = 1, i do table.insert(n, math.random()) end
        local val = math.random()
        local mtn = MultiDimTable(i)
        mtn[n] = val
        assert( mtn[n]           == val )
        assert( mtn(n)           == val )
        assert( mtn{unpack(n)}   == val )
        assert( mtn[{unpack(n)}] == val )
    end
end


Original Page Contents

do             
    local meta = {}

    local xlate = {[0] = "x", "y", "z"}

    function meta:__index(key)
        if xlate[key] then return rawget(self, xlate[key]) end
    end

    function meta:__newindex(key, val)

      if xlate[key] then
        rawset(self, xlate[key], val)
      else
        rawset(self, key, val)
      end
    end
  
    function Vector(x, y, z)
      return setmetatable({x=x, y=y, z=z}, meta)
    end
end

a = Vector(1, 2, 3)

=a.x
1
 =a[0]
1
 a[2] = 7
 =a.z
7

How does this work?

Firstly data is ONLY stored in x,y,z values in out table created via Vector(1, 2, 3). We use a dummy table and a lookup table to achieve our goal.

TBC....


RecentChanges · preferences
edit · history
Last edited March 11, 2008 11:58 pm GMT (diff)