Play With Lua!

A short trick: transparent containers

without comments

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.

Written by randrews

July 24th, 2011 at 12:35 am

Posted in Uncategorized