Lua, why are you like this?

Okay so I like LÖVE for making games, and have used it for quite a few of them at this point.

I like that it gives me a bunch of useful primitives for making games, and then just gets out of my way. And I like that it has a simple build process where it isn’t too difficult to make a cross-platform build and continuous deployment system that also lets me do continuous deployment to itch.io or whatever.

And I also like that Lua is a fairly easy language to learn, with a simple syntax. But there’s a few things about it which are just baffling or annoying to me.

And I’m not talking about the 1-based arrays! (That’s annoying in a couple of situations but for the most part it doesn’t really matter, at least not to the extent that people make a big deal about it.)

Anyway, right now I’m procrastinating on working on a new game for a game jam, so I’d might as well just finish this rant.

Built-in metatables

In Lua, there are basically three data types: numbers, strings, and tables.

A table has, on its surface, two parts: a key-value storage (like a Python dict) and a metatable, which is roughly the equivalent of a prototype in JavaScript. It provides a bunch of useful stuff like operator overloads (which can override everything including getting and setting), default value (“index”) lookups (which can be used for simple object inheritance), and lets you also set keys and/or values as being weak references.

Some tables are used roughly as namespaces to bundle up related functionality. For example, a bunch of string functions are stored on the string index table, so you get things like string.upper(theString). String objects also get the index table set as their index in the metatable (despite not actually being tables), so for example:

foo="bar"
print(foo:upper)

is more or less equivalent to

foo="bar"
print(string.upper(foo))

But there is one notable exception to this: tables declared inline with or without implicit indexes (and which is where the 1-based indexing comes from in the first place), do not get a metatable set, when there are a lot of useful functions for manipulating them on the table table!

You would expect this to work:

foo={1, 2, 3, 4, 5}
foo:insert(6)

but it doesn’t; instead you need to do:

table:insert(foo, 6)

So, this is a bit annoying.

Granted, it makes sense for not all tables to get the table metatable as their default index, but it seems like the default namespace table should not get it but all others should? Or at least ones declared inline. I dunno. This just feels like something that wasn’t thought out very well in the language and we’re kind of stuck with it.

I guess it’s easy enough to write a wrapper function to apply it, if that’s something you want. (This is a thing you do a lot in Lua.)

Version confusion

There are a bunch of versions of Lua out there. Right now the current version is 5.3. LÖVE uses LuaJIT, which is a modified version of 5.1. There are many differences between 5.1 and 5.3; for example, in 5.1, unpack is provided as a global (although not documented as one!), while in 5.3 it’s strictly part of the table namespace. And LuaJIT adds the bit32 package from 5.2, but calls it bit instead (but of course the docs for it are at bitop.luajit.org.

The way the documentation is laid out makes it hard to tell what’s what, and how to find these things out.

I wish the scoping rules were inverted

This is more of a minor gripe; Lua’s scoping rules default to being like Javascript, where you have to declare something to be in the local scope or else it defaults to the global scope. I wish all variable declaration were explicit instead, which would make it unambiguous and an error if you forget.

Fortunately there’s an easy hack to get that; I start all my projects with:

setmetatable(_G, {
    __newindex = function(_, name, val)
        error(string.format("attempted to write %q to global variable %s", val, name), 2)
    end
})

which is basically how you disable writes to the global scope. (The __newindex function is what’s called when you try to assign a value to a table which doesn’t have that key.) Unfortunately, a lot of Lua libraries end up writing to the global scope, but this is IMO an error in those libraries and whenever I come across something that does this I submit bug fixes.

nil as a terminator, sometimes

If you have a sequence, and any value in the sequence is nil, that acts as a terminator for the entire sequence when using ipairs. But not when using pairs.

Also the # operator doesn’t actually account for this:

foo={1,2,3,4,5}
-- #foo is now 5
foo[3]=nil
-- #foo is still 5
for i,v in ipairs(foo) do print(i,v) end
-- prints:
-- 1   1
-- 2   2
for i,v in pairs(foo) do print(i,v) end
-- (probably) prints:
-- 1   1
-- 2   2
-- 4   4
-- 5   5

Heck, the # operator is ridiculously inconsistent

foo={1,2,3,4,5}
foo[3]=nil
table.insert(foo,6)
=#foo
-- 6
for k,v in ipairs(foo) do print(k,v) end
-- 1    1
-- 2    2
foo[6]=nil
=#foo
-- 5
foo[10]=5
=#foo
-- 10
foo={1,2}
foo[199]=23
=#foo
-- 2

So many things to build…

Lua doesn’t come with much in the standard library, and you end up having to implement a bunch of it yourself. Even things that you shouldn’t have to. For example, there’s no default way to copy an array (and getting it right can be hard, if you want a deep rather than a shallow copy), and there’s no equivalent to Python’s zip, which would also be really handy.

Semicolons are weird

So, Lua’s grammar is such that semicolons are totally optional, except where they aren’t.

If you get a newline where there can be a new statement, that starts a new statement. For example,

foo=(1 +
    2)
bar = 3

parses as two statements, equivalent to

foo=(1 + 2)
bar = 3

But you can also have multiple statements on a single line; this also works:

foo=1 + 2 bar = 3

; is technically an empty expression that forces parsing a new statement. There is exactly one time when it’s actually necessary: if the next statement starts with a (.

a = b ; (foo or baz)(5)

is distinct from

a = b (foo or baz)(5)

Fortunately, situations like that are uncommon and if you’re causing it to happen you should probably feel bad anyway.

Oh but even though ; is an “empty statement” it isn’t equivalent to a statement – ;; is not valid.

= vs. =

In a regular statement, = is the assignment operator; if you have lvalue = rvalue then it assigns the value of rvalue to the value of the variable that lvalue dereferences to.

But in a table assignment, = is name = rvalue, and name is implicitly converted into a string (similar to the => operator in Perl).

If you want to override this, you have to surround the name with brackets, like:

a = "Hello"
foo = { [a] = "World" }
print(a, foo[a])  -- outputs "Hello World"

This does make table initializers easier to write, but discovering the syntax is a little weird and non-obvious. (Although it does make a certain sort of sense; foo.a uses a as a key lookup whereas foo[a] uses the value of a as a key.)

Of course there’s an exception to this: if the key is a number it just fails as a parse error; foo = {1=2} does not work, so you have to instead do

foo = {[1] = 2}

Of course since 1 and "1" are not equal (thankfully) you can have:

foo = {[1] = 5, ["1"] = 10}
for k,v in pairs(foo) do
    print(k,v)
end
-- outputs:
-- 1    10
-- 1    5

Truthiness is consistent

In Lua, exactly two things are equivalent to false: false and nil.

This is actually kind of nice because existence implies truthiness; but it’s confusing to have 0 be true. (As well as empty tables and strings.)

I guess this is more a complaint about how it’s different from other things and not a problem with the language itself though.

REPL inconsistencies

The Lua 5.x standard REPLs support readline. So cursor and edit keys work fine.

The LuaJIT one does not.

So if you want to verify Lua behavior and want to ensure it’ll be the same in LÖVE, you have to eschew the ability to edit commands in-place. Which is annoying.

To be fair that’s more of an ecosystem issue than a language issue though.

I do like how the REPL provides = as a shortcut to evaluating the remainder of the statement as the RHS of an assignment and then prints it out, to make up for how Lua statement syntax works.

String joins live on table for some reason

Want to join a bunch of strings together with a separator? Use table.concat.

At least this works on numbers as well as strings.

The concatenation operator

While we’re at it, Lua provides a string concatenation operator, .., which coerces both sides of the expression to a string. Unless that value is nil or a boolean, in which case it barfs.

Also, only numbers can be directly coerced to a string. To convert anything else you need to use tostring, which does not live on the string table. (But for anything complicated you should be using string.format anyway.)

In summary…

I really wish PyGame were updated to SDL2 and it weren’t such a nightmare to package Python apps for standalone distribution.

Comments

Before commenting, please read the comment policy.

Avatars provided via Libravatar