A short trick: transparent containers
I thought of this today, and I thought it might make an interesting short post. One of the small annoyances of Lua is that it doesn’t have a lot of convenient support for arrays. You can implement arrays with tables, of course, but they’re still tables: they don’t have default metatables (so you can’t do things like join
), they don’t pretty-print (so it’s a hassle to see what you have), and there’s still only one iterator (using ipairs
with a for loop). You can do it but it’s not exactly convenient. So I started thinking about how exactly I’d add all that stuff.
Creating an array
First, let’s recognize that not all tables are going to have this behavior, and that they actually shouldn’t. An array is a CS concept, an abstract data structure, that just happens to be a primitive in most languages and implementable with a table in Lua. We want to make a way to create arrays, and some handy default behaviors for arrays, and that’s it.
So what I did was make a table to be used as the metatable for all arrays, called _array
. Then I made an array
function, global, that would make a table and set that as its metatable. Then, to save a little typing, I aliased that function to A
:
_array = { -- Functions go here } function array(...) return setmetatable({...}, _array) end A = array
So now I can make an array by saying array(1, 4, 9)
, or more likely (because short is good for simple concepts), A(1, 4, 9)
.
Pretty-printing
Making an array and then seeing table: 0x100116290
when I print it out is a little less than useful. It’s really helpful for exploratory programming (not to mention debugging) if I don’t have to write a for loop by hand whenever I want to inspect an array. So, the first thing I’m going to add is a __tostring
metamethod. This is so easy to do using the Lua standard library that I’m surprised I even have to, that it’s not built in:
__tostring = function(tbl) local strings = {} for k, v in ipairs(tbl) do strings[k] = tostring(v) end return '[' .. table.concat(strings, ', ') .. ']' end
This metamethod gets called whenever something needs to become a string; notably, because tostring
told it to. We feed everything in the array into tostring
, then join it all up with commas and put it in brackets. This function will get a bit smaller shortly though.
Iteration
There are two things I want to provide for iteration. First, I want to have an each
function that I pass a function to (along with some arguments) and it will invoke it on every element in the array. Pretty standard stuff:
each = function(list, fn, ...) local r = array() for k,v in pairs(list) do r[k] = fn(v, ...) end return r end
This returns another array containing all the return values of the invocations, so I can do stuff like this:
A(1,4,9):each(function(v) return v*v end) -- ==> [1, 16, 81]
Since it’s created with array
, I can chain them together; the return value will also support each
. And of course, now that we can do this, we can refactor __tostring
to this:
__tostring = function(tbl) local strings = tbl:each(tostring) return '[' .. table.concat(strings, ', ') .. ']' end
Transparency
The other kind of iteration I want to do is the neat trick: I want to be able to treat an array of objects just like an object. Suppose I have a bunch of objects, like strings, and I want to invoke a string method on them, like this one I shamelessly stole from Rails:
function string.ordinalize(str) local num = tonumber(str) local endings = {"st", "nd", "rd"} if num >= 11 and num <= 13 then return num .. "th" elseif num % 10 > 0 and num % 10 <= #endings then return num .. endings[num % 10] else return num .. "th" end end
It would be neat if I could make the array take any message it doesn’t understand and pass it along to all its elements, so this:
foo:ordinalize()
is the same as this:
A(foo[1]:ordinalize(), foo[2]:ordinalize(), foo[3]:ordinalize())
So let’s do that. This has a lot in common with each
, so maybe we can write this as a special case of that. We’ll make a function that calls a method on an object given its name, pass that inte each
, and return the result.
A method call (using the colon) is just a lookup of a name followed by calling that function, with the object as the first argument, like this:
function send_msg(obj, name, ...) return (obj[name])(obj, ...) end
So, our function to create a function like this and send it to each
is this:
send = function(msg) return function(list, ...) local function fn(v, ...) return (v[msg])(v, ...) end return _array.each(list, fn, ...) end end
What we’ll do in __index
is call this (with a name) to get a function (method) that takes the array and any extra arguments, and then return it (so that the caller can pass into it an array and some extra arguments). Here’s the finished __index
, that handles that and each
:
__index = function(t, name) if name == 'each' then return _array.each else return _array.send(name) end end
Now, we can do stuff like this:
A(1,10,33):each(tostring):ordinalize() -- ==> [1st, 10th, 33rd] A('foo','bar','baz'):sub(1,1) -- ==> [f, b, b]
Next steps
There are a lot more functions that it would be handy to do to arrays other than just each
and passing through method calls. We could want to reverse them, or partition them, or filter elements out. inject
is handy in more places than you’d expect. Ruby has a really complete set; if you want to try adapting some methods from Ruby’s Enumerable library into this, the code is here.