Lua Gotchas
Lua is an awesome language, but it has some unusual design choices that can cause endless frustration to beginners (and experts). This post goes over these gotchas1.
Table of Contents
- Variables are Global Unless Specified with
local
- Functions are Global Unless Specified with
local
a.func(...)
vsa:func(...)
- Tables are Both Lists and Dictionaries
- The Behavior of
ipairs
vs.pairs
- The Behavior of
#
Variables are Global Unless Specified with local
In Lua, variables are globally scoped by default. This has lead to many problems for me, since a temporary variable used by a function can be overwritten by anything called by that function. Here is a somewhat contrived example, where two functions both use the variable name temp
as an intermediate value, thereby overwriting each other. Running this snippet and entering 3
, and 4
produces the result 8
, when it should return 7
. Try this snippet on repl.it!
function readNumber()
print("Type a number:")
temp = io.read("*number")
return readAnotherNumber() + temp
end
function readAnotherNumber()
print("Type another number:")
temp = io.read("*number")
return temp
end
We can fix this by adding the local modifier to the temp
assignment, as follows:
function readNumber()
print("Type a number:")
local temp = io.read("*number")
return readAnotherNumber() + temp
end
function readAnotherNumber()
print("Type another number:")
local temp = io.read("*number")
return temp
end
Here’s something from one of my projects - its a backtracing search. Can you spot the bug?
function best_next_action(actions_so_far)
if #actions_so_far == SEARCH_DEPTH then
return nil, eval_actions(actions_so_far)
end
best_action, best_score = nil, -math.huge
for next_action=-1, 1, 2/(STEERING_BINS - 1) do
table.insert(actions_so_far, next_action)
_, score = best_next_action(actions_so_far)
if score > best_score then
best_score = score
best_action = next_action
end
table.remove(actions_so_far)
end
return best_action, best_score
end
I forgot to make best_action
and best_score
local, thereby making recursive calls destroy the caller’s value. This bug took hours to find. score
should also be made local, though in this situation it’s not a problem.
I recommend you do yourself a favor and get in the habit of making every variable a local by default.
Functions are Global Unless Specified with local
Similar to variables, function in Lua are global by default, unless you specify the local
keyword. This can cause an issue if you have an inline function that has the same name as a global function, or if you try to return functions in a closure. Here’s an example where this is an issue.
function make_counter(i)
function incr() i = i + 1 end
return function()
incr(); return i
end
end
local a, b = make_counter(0), make_counter(0)
print(a(), a(), a())
print(b(), b(), b())
This is supposed to print out 1 2 3
twice, but instead prints out 0 0 0
and 4 5 6
, because the incr
function ends up being shared between the two instances of the counter. Try the code out on repl.it.
The code can be fixed by simply adding the local
keyword to the incr
function.
function make_counter(i)
local function incr() i = i + 1 end
return function()
incr(); return i
end
end
a.func(...)
vs a:func(...)
Tables in lua are just dictionaries from keys to values. The .
operator can be used to get the value for certain keys. To simulate object-oriented programming you could have a table represent an object, with fields as entries. Methods would also be entries, but the value would be a function that takes in the object itself as the first argument.
local point = {
x = 0, y = 0,
print = function(self) print(self.x, self.y) end
}
point.print(point)
This syntax can feel redundant. Because it’s such a common pattern, Lua introduces a special :
operator. You should think of a:func(...)
as equivalent to a.func(a, ...)
.
However, this can lead to mixups if you accidentally use the wrong operator. If you wrote .
but were supposed to write :
, then you might get the error message attempt to index local 'self'
. Otherwise, you might get some confounding type error further down the road. This is further complicated by the fact that some libraries want you to use .
for objects, and thus store the variable self
in a closure.
Tables are Both Lists and Dictionaries
In many languages, dictionaries and lists are separate datatypes, but in Lua these concepts are merged into one datastructure - the table. The Table is effectively a dictionary where keys and values can be anything except nil
(assigning a value to be nil
is the same as deleting that key, while nil
simply can’t be a key). Thus one data structure can act as both a hashmap and a list, and even be both at the same time.
The Behavior of ipairs
vs. pairs
pairs
iterates over every key in a table, whether it’s an array-like key or a map-like key. ipairs
on the other hand, has the unusual behavior that it starts at the key of 1
and then keeps incrementing the key until the value is nil
. Take a look at the following cases below that shows the unusual results this behavior can create. You can run the code on repl.it.
function ipairs_print(t)
for _, v in ipairs(t) do print(v) end
end
ipairs_print({1, 2, 3, nil, 4}) -- Prints 1, 2, 3
ipairs_print({nil, 1, 2, 3, nil, 4}) -- Prints nothing
ipairs_print({[0]=1, [1]=2, [2]=3}) -- Only prints 2, 3
- If you have a nil in the middle of your array,
ipairs
will stop at that nil. - If the value at index
1
isnil
, then ipairs will print nothing. - If you mistakenly use index
0
as the first element of your array, then ipairs will skip that element.
The Behavior of #
The #
operation is confusing in that it can actually return different results for the same table, depending on how the table was constructed. From the manual:
The length of a table
t
is defined to be any integer indexn
such thatt[n]
is not nil andt[n+1]
is nil.
You may notice that, if your array has holes, there can be multiple such indices n
, therefore the #
operator can return any of those indices as a valid answer. Below is an example that showcases this unusual behavior, which you can run on repl.it.
print(#{[1]='a', [2]='a', [4]='b'}) -- Prints 4
print(#{[1]='a', [2]='a', [5]='b'}) -- Prints 2
print(#{'a', 'a', nil, nil, 'b'}) -- Prints 5
The bottom line is, unless you know for sure that your array doesn’t have holes, don’t use #
.
-
More gotchas can be found at this link - http://www.luafaq.org/gotchas.html ↩