lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


Am 24.06.2016 um 20:32 schröbte Dirk Laurie:
[1]
Tables have individual metatables. Tables freshly constructed
by a table literal have none. The following is an error:

({24,6,2016):concat"/"  --> (not yet) 24/6/2016

Would it not be nice if tables that have no metatable fall back
to {__index=table}? [2] Only table.pack does not have a table as
first argument, and we have a precedent for that in 'string',
which is also the __index of a metatable but contains functions
cannot be used with object notation.

Strange thing, I've been thinking about this as well, but I'm coming from a different direction:

As I've mentioned elsewhere I'm not a big fan of the `table.pack()` function, but one nice thing about it is that we now have an official way to represent sparse tables (with that extra `n` field that `table.pack()` sets). Strange thing is that `table.unpack()` *does not* honor that `n` field although those two functions appear to be inverses of each other. What `table.unpack()` *does* honor is the `__len` metamethod, so I thought maybe `table.pack()` should set a metatable on its result. Actually, most (all?) of the table functions in Lua 5.3 respect metamethods now, so I wondered what it would take to make them work with sparse tables as well, and it's not that much:

I already mentioned `__len`. For `t[#t+1] = x` to work you'll also need a `__newindex` metamethod that updates `n` if the key is an integer greater than `n`. Now `table.insert()` (mostly) works as well. `table.remove()` needs some wrapping because removing an existing element does not trigger `__newindex` (and assigning `nil` to `t[t.n]` probably should not shrink `n` if `t` is allowed to have `nil` values in it, anyway). `table.pack()` is wrapped to set the metatable on it's result, and `table.sort()` gets a new default comparator that moves `nil` values to the end of the array. And that's mostly it!

I've attached a proof of concept. I've also added an `__index` metamethod for OO syntax and `__call` for table iteration via `for i,v in array do ...` (no need for `__next`), although I don't care much about those two features.

Back to the original proposal:

Lua already knows how many elements -- including `nil`s -- should be added to a table in a lot of cases, e.g.

    { ... }  -- should be `select('#', ...)`
    { a = 1, ... }  -- still `select('#', ...)`
    { 1, f() }  -- should be 1 + number of return values
    { 1, 2, 3 }  -- should be 3
    { 1, nil, nil } -- ditto
    { 1, 2, [4]=4 }  -- 4
    { 1, 2, [x]=4 }  -- 2, or x if x is an integer > 2
    { 1, 2, [x]=nil }  -- same

 and could set `n` and the metatable for us automatically.

The only case (I can think of) that's really missing is the empty table:

    local t = {}  -- array with n=0 and metatable, or just plain table?

So far I've thought of two possible solutions:

1.  new syntax sugar for creating arrays  (`local t = []`?)
2.  a special case in the parser for a literal `{ n = 0 }`
3. a constructor function for creating arrays (hopefully somthing shorter than `local t = table.pack()`!)


TL;DR: I wouldn't set a default metatable on _all_ tables, only those that the Lua parser deems array-like. And maybe that can also solve (or at least mitigate) the array-with-holes debacle ...

And now, discuss! :-)

Philipp



[1] Since feature request season seems to be open.
[2] Sean will no doubt be able to cite when this exact proposal
was first made, unless it was before 2009.



local M = {}
local Meta = {
  __len = function( t )
    return t.n
  end,
  __newindex = function( t, k, v )
    if type( k ) == "number" and k % 1 == 0 then
      if k > t.n then t.n = k end
    end
    rawset( t, k, v )
  end,
  __index = M,
  __call = function( self, _, var )
    var = (var or 0)+1
    if var <= self.n then return var, self[ var ] end
  end,
}


-- make a plain table an array
local function setmeta( t )
  local mt = getmetatable( t )
  if mt == nil then
    t.n = #t
    setmetatable( t, Meta )
  elseif mt ~= Meta then
    error( "conflicting metatable", 3 )
  end
end


-- copy all functions from the table library
for k,v in pairs( table ) do
  M[ k ] = v
end


-- replace some of the original table functions ...

-- table.pack()
function M.pack( ... )
  return setmetatable( table.pack( ... ), Meta )
end


-- table.remove()
function M.remove( t, ... )
  setmeta( t )
  local n, v = t.n, table.remove( t, ... )
  if (... or n) > n then
    t.n = n
  elseif n > 0 then
    t.n = n - 1
  end
  return v
end


-- table.insert()
function M.insert( t, ... )
  setmeta( t ) -- in case you want to insert `nil`s
  return table.insert( t, ... )
end


-- table.move()
function M.move( a1, f, e, t, ... )
  if select( '#', ... ) == 0 then
    setmeta( a1 )
  else
    setmeta( ... )
  end
  return table.move( a1, f, e, t, ... )
end


-- table.sort()
local function cmp( a, b )
  if a == nil then
    return false
  elseif b == nil then
    return true
  else
    return a < b
  end
end

function M.sort( t, ... )
  if select( '#', ... ) > 0 then
    return table.sort( t, ... )
  else
    return table.sort( t, cmp )
  end
end


-- return module
return M