Thursday, January 8, 2009

Initial impressions of Lua

During the long weekend, I've spent a few hours on messing up with Lua - after some readings I finally got seduced enough to give it a try.

As an exercise, I went over porting part of the reader of the "message template" file (the latter I grabbed from libopenmv) that I previously wrote in Ruby (about 15K of code to generate the semi-functional Ruby class definitions). The port now reads the defininions into a bunch of interconnected tables, however it's not too big either - just about 3K:

First, a helper method (I miss ruby String.split() somewhat):


function string.gather(str, pat)
local tab = {}
for match in str:gmatch(pat) do
table.insert(tab, match)
end
return tab
end


Then goes my experiment with an iterator-within-a-coroutine-in-OO-like-thing to get the tokens in batches:


TemplateReader = function(filename)
local worker = function(fname)
for line in io.lines(fname) do
-- get rid of comments, [rl]trim, remove trailing and multispaces
local line2 = line:gsub("//.*$",""):gsub("^%s*","")
:gsub("%s+$",""):gsub("%s+"," ")
local tokens = line2:gather("([^%s]+)")
if(#tokens > 0) then
coroutine.yield(tokens)
end
end
end

local me = {}
me.fname = filename

me.coworker = coroutine.create(worker)
me.tokens = function(me)
local ok, tok = coroutine.resume(me.coworker, me.fname)
return tok
end

return me
end



And then go the "conceptual piece" readers. They were in separate classes in Ruby version, but given the pretty similar structure for all the packets, I think I might not need it.

Here we go, in reverse order. The bottom-most part is the actual "main" body:


tr = TemplateReader(filename)

version = tr:tokens()
print("version: " .. version[2])
packets = {}
for packet in PacketTemplateRead, tr do
print("Packet obtained: " .. packet.name)
table.insert(packets, packet)
end

print("done, total: " .. #packets)


Now, let's see how we read the packets:



PacketTemplateRead = function(tr)
local start = tr:tokens()
local pt = nil
local finished = false

if (start and #start == 1 and start[1] == "{") then
local pkt_info = tr:tokens()
pt = {}
pt.blocks = {}
pt.name = pkt_info[1]
repeat
local tok = tr:tokens()
if tok[1] == "{" then
table.insert(pt.blocks, PacketBlockTemplateRead(tr))
else
finished = true
end
until finished
elseif start then
print("start[1] is: " .. start[1])
print("foobar!")
end
return pt
end



So, let's see how we read the block...


PacketBlockTemplateRead = function(tr)
local b = {}
local toks = tr:tokens()

b.name = toks[1]
if toks[2] == "Single" then
b.count = 1
elseif toks[2] == "Multiple" then
b.count = toks[3]
elseif toks[2] == "Variable" then
b.count = -1
else
print("Unknown block count " .. toks[2]
.. " for block " .. toks[1])
-- FIXME: need an exception ?
end

b.fields = {}
for field in PacketFieldRead, tr do
table.insert(b.fields, field)
end
return b
end


The only thing that is left mysterious, is the packet field, which is the "name / type / length" kind of thing:


PacketFieldRead = function(tr)
local toks = tr:tokens()
local field = nil
if not (toks[1] == "}") then
field = {}
field.name = toks[2]
field.type = toks[3]
field.len = toks[4]
-- print("Got field name: " .. field.name .. " type " .. field.type)
end
return field
end


This reads the message template visually several times faster than Ruby, in barely noticeable fractions of a second on my laptop.

Which *may* make this a reasonable candidate not to store the generated code anywhere at all, and just keep the message template file. Though, my experiences with the Ruby autogenerated in-memory code were that in case there's an exception inside that code, it's a royal PITA to debug - so the question of how to do error handling would need a better thought.

Overall impressions of translating this from looking at the Ruby version:

1) 1-based arrays are a pain for the C/Ruby/...-infected zero-based brain to switch to.
In fact, I did create a couple of bugs precisely while retyping usage of the elements of the array. And there's hardly twice than that places where I use the indices :) So - this is a big warning sign to myself. Just that it is not easy - not that "0" or "1" based are better or worse in all cases.

2) having functions as first class citizens feels really easy-going after Ruby.

3) coroutines are cool. the yield/resume couple interaction creates the feeling that the world is something you can turn inside-out and back - and these two do precisely that. Very odd feeling. Though I would not say that my use of coroutine-within-the-iterator is a terribly good idea.

4) table-based OO feels a big bit like in Javascript.

5) you notice "not x == y". I find it more readable compared to ~=, and less mentally confusing with =~ (Perl-ism to match on a regexp).

6) finally a minor one but probably the most annoying - after being used to write print "Var: #{x}\n" in Ruby, it is pretty bad to have to write print("Var: " .. x .. "\n") in Lua. And the main annoyance is the parens. I know they do create the ambiguity and pain for the parser, but the lack of them is so conweeeeenient! :)

Overall, seems to be pretty usable and sweet language, and given its speed, tiny size of the base kit, and the ease of integration with C, as well as a JIT compiler, which in my microbenchmarks did show pretty nice results - I like it

p.s. don't ask me why I mention Python only here. I am still trying to wrap my inner nerves around the whitespace thing.

No comments: