Conventions and classes, not new syntax (was: Re: Shorthand for appending to a table (was Re: 5.2 feature))

classic Classic list List threaded Threaded
1 message Options
Reply | Threaded
Open this post in threaded view
|

Conventions and classes, not new syntax (was: Re: Shorthand for appending to a table (was Re: 5.2 feature))

Paul Chiusano
I've found that a lot of desirable features can just be implemented in
stock Lua, just by creating classes and possibly adopting some
conventions. Why add a special syntax for table appends when you can
just implement the feature yourself in Lua, with the exact semantics
you want, in about 10 minutes?

If you need to track the number of entries in a table, you can just
write a class which does it. If you want tables to allow negative
indexing (to count back from end), just implement it as a class. If
you want a table which tracks the number of entries, just implement it
as a class. If you want the pairs function to work for objects that
you've constructed and not just raw tables, just overwrite the pairs
function to look for, say, a __pairs metamethod, etc. Usually, you can
have the syntax for whatever you add be identical to doing everything
with raw tables and with stock Lua (if that's what you want), the only
difference might be that when you construct, say, your souped-up table
which tracks the number of entries, you'll need to do Map {a=1, b=2}
instead of just {a=1,b=2}. Even that could be addressed with *very
simple* token filtering!

For instance, included is a List class whose metamethods allow you to
access the last element with t[Last], to append with t[End] = val, and
which also supports negative indexing. It just uses metamethods and a
simple convention to achieve the effect.

Here's an example of its use:

list = require 'List'
List, End, Last = list.List, list.End, list.Last
a = List(1,2,3,4,5)   -- List is a vararg function that returns a List object
= a
List(1, 2, 3, 4, 5)

Negative indices count back from end:

= a[1], a[-1]
1       5
= a[-a.size()] -- -size gets first index
1

Assigning to nil deletes a slot.

a[-1] = nil
= a
List(1, 2, 3, 4)
a[-2] = nil
= a
List(1, 2, 4)

You can also get last index with special value 'Last':
= a[Last]
4

and you can append using a[End] = val

a[End] = "hello world!"
= a
List(1, 2, 4, hello world!)

End and Last aren't keywords, they're just unique values:

= End, Last
table: 0x304010 table: 0x303ae0

The metamethods just adopt the convention of treating these values as
special. 'Last' represents the last valid index in the List, and 'End'
is 'Last+1'. Since these values are unique to the List package,
there's no chance they'll accidentally be used someplace else.

And this is all just pure Lua! There's no special-purpose syntax, and
if you want your lists to have long meaningful names, you don't have
to type that name twice just to append. Of course, you might not want
these exact semantics (like maybe you don't want to do anything
special to negative indices, or maybe you want assigning to nil to do
something else), but these are easy to adapt.

Here's the code for List.lua. It uses the 'table of closures'
implementation of classes discussed in 16.4 of programming in Lua, but
it could probably be adapted to use the other way of implementing OO,
using the a:method() syntax.

-----
-- List.lua

-- These are just unique values to dispatch on
local End = {}
local Last = {}

local insert, remove = table.insert, table.remove

local function List(...)

  -- this is the actual list upvalue
  -- we could explicitly track the size, too, if we don't want to rely on '#'
  local mElements = {...}

  -- public interface table
  local public = {}

  -- this table is used to translate special indices: a[Last], a[End]
  local dispatch = {
     [End] = function() return #mElements+1 end,
     [Last] = function() return #mElements end
  }

  local function translateNegative(ind)
     return ind > 0 and ind or #mElements+ind+1
  end

  local function getIndex(v)
     local tmp = dispatch[v]
     return tmp and tmp() or translateNegative(v)
  end

  function public.add(val, index)
     index = index or #mElements
     insert(mElements, getIndex(index), val)
  end

  function public.remove(index)
     index = index or #mElements
     return remove(mElements, getIndex(index))
  end

  function public.get(index)
     return mElements[getIndex(index or #mElements)]
  end

  function public.set(index, val)
     if val==nil then return public.remove(index) end
     mElements[getIndex(index)] = val
     return public
  end

  function public.size()
     return #mElements
  end

  function public.__tostring()
     local buf = {}
     for _,v in ipairs(mElements) do
        buf[#buf+1] = tostring(v)
     end
     return "List("..table.concat(buf, ", ")..")"
  end

  public.__index = function(_,ind) return public.get(ind) end
  public.__newindex = function(_,ind,val) return public.set(ind,val) end

  setmetatable(public, public)
  return public
end

return {List=List, End=End, Last=Last}