Lua introduction for confirmed developers
Generalities about Lua
Lua, not LUA
This is a Portuguese word meaning "moon", not an acronym.
Lua design
It is useful to remind what Lua was designed for, as long with the fact that it dates from 1993:
- Easy to embed and to interface from other languages.
- Highly portable. The con is a lightweight and somewhat lacking API.
- A small set of features allowing extensible semantics to support multiple paradigms, from functional programming to object programming. For example, while inheritance is not natively supported, it can be easily set up through metatables. The same features allow events in civ5 to behave both as functions and objects with Add and Remove methods.
- A beginners-friendly syntax.
Performances
Lua is an interpreted and dynamically typed language. So as usual with those kind of languages expect a 10 to 100 order of magnitude in performances decrease when compared to compiled and statically type languages like C, C++ or C#. While it is possible for Lua-like languages to drastically improve the performances through more or less sophisticated means (parse once, Jit compiler, type inference to statically resolve code), no such solution has been used in Civ5 (aside, probably, of the caching). And while an external Jit library exists, it causes compatibility problems with most mods using Lua.
That being said, Lua is still pretty good for this family of languages, better than the Python implementation from Civ4 for example. And all in all, it is fast enough for most of what modders want to achieve without the need to compromise code elegance, readability and maintainability for performances. Anyway, remember: "premature optimization is the root of all evil [ 97% of the time ]" (Donald Knuth).
Syntax cheatsheet
Basics
-- Literals
local x = 5
local x = 5.0
local x = nil -- A nil value is the same as an undefined value
local x = true
local x = false
-- Strings
local x = "five" -- String
local x = 'five' -- String (no difference with "")
local x = "five\n" -- Escape characters with "\".
local x = [[Five is a number.
Did you know?]] -- Multi-line string: equivalent to "Five is number.\nDid you know?"
-- Globals versus locals
x = 5 -- A global variable
local x = 5 -- A local variable
-- Operators :
local x = a or b
local x = a and b
local x = "hello ".."world" .. "!" -- concatenation: two dots
local x = "five is "..5 -- concatenation with implicit cast: "five is 5".
local x = (a + b - d) * f ^ g % h -- ^ is exponentiation, % is modulo
local x = a == b -- equality
local x = a ~= b -- inequality
local x = ((a <= b) == not (a > b)) == ((a >= b) == not (a < b))
local x = "abc" < "def" -- Works but may not respect Unicode, beware for non-western languages. Use the Locale methods instead.
-- No increment operator (++ --), no binary-assign operator (+= *=), no ternary operator (? :), no coalescence operator (??)
Tables and multiple assignments
-- Table declaration
SomeTable = {}
SomeArray = { "abc", "def" }
-- 1 => "abc", 2 => "def"
SomeComplexTable = { name = "John", surname = "Doe", age = 25, 43 }
-- "name" => "John", "surname" => "Doe", "age" => 25, 1 => 43.
-- Member access (all equivalent)
SomeTable.SomeMember = 5
SomeTable["SomeMember"] = 5
-- Multiple assignment
local x, y = "abc", "def"
local x, y = unpack(SomeArray) -- equivalent to the former statement since SomeArray contains "abc" and "def".
Functions
-- Functions declarations (all equivalent)
function HelloWorld(a, b) print("Hello world") end
HelloWorld = function(a, b) print("Hello world") end
-- Functions are first-class objects and can be assigned like any other value!
-- Methods declarations (all equivalent)
SomeTable.HelloWorld(a, b) print("Hello world") end
SomeTable.HelloWorld = function(a, b) print("Hello world") end
-- Instanced methods declarations (all equivalent)
SomeTable.HelloWorldInstanced(self, a, b) print(self.SomeMember) end
SomeTable:HelloWorldInstanced(a, b) print(self.SomeMember) end
-- Functions and methods calls
HelloWorld(a, b)
SomeTable.HelloWorld(a, b)
-- Instanced methods calls (all equivalent)
SomeTable:HelloWorldInstanced(a, b)
SomeTable.HelloWorldInstanced(SomeTable, a, b)
-- Variable arguments
function SomeFunc(a, b, ...)
print(a)
print(b)
for i, v in ipairs(arg) do -- arg is a keyword for a table containing the variable arguments
print(v)
end
end
SomeFunc(1, 2, 3, 4, 5) -- prints 1, 2, 3, 4, 5
SomeFunc(1, 2) -- prints 1, 2
Control flow
-- If
if name == "Wu Zetian" then
print("Hello China")
elseif name == "Napoleon" then -- "elseif", not "else if". Beware C developers!
print("Hello France")
else -- no "then" after "else"!
print("Hello unknown")
end
-- For loop (with counter)
for i = 1, 8 do print(i) end -- Prints 1, 2, 3, 4, 5, 6, 7, 8
for i = 1, 8, 2 do print(i) end -- Prints 1, 3, 5, 7
-- For loop (with iterator)
for playerID, pPlayer in pairs(Players) do -- Prints 1 Wu Zetian, 2 Napoleon, ...
print(playerID, pPlayer:GetName())
end
for plot in Plots() do -- Prints 0 0, 0 1, 0 2, ...
print(plot:GetX(), plot:GetY())
end
-- While (i will be 5 in the end)
while i < 5 do
i = i + 1
end
--Repeat until (i will be 5 in the end)
repeat
i = i + 1
until i == 5
Specificities
Type system
You can get a variable's type by using the type function. It takes any value and returns a string. Here are the possible results:
- nil: Lua makes do distinction between an undefined variable and a variable with a nil value. Assigning nil to a variable or table member is equivalent to undefining it.
- number: a 64 bits floating-point number. Lua has no support for integers and only use floating-points.
- string: your usual string of characters. In Lua all strings are interned. This speeds up comparisons but it makes allocation of new strings slower.
- boolean: true or false obviously.
- table: any table.
- userdata: objects defined in C for use in Lua. They may not be enumerated through pairs and other limitations. Now the Civ5 API tend to return regular tables whose metatable is of type userdata, this allows us to use the provided objects as regular Lua tables.
- function: remember that functions are first-class objects in Lua.
- thread: coroutines actually. Lua does not support threads.
Strict equality comparisons
Two values are equal only if their types are the same. This is stricter than C and far stricter than PHP for example.
- 4 is not equal to "4". (number type and string type). In PHP they would be equal.
- 0 is not equal to false (number type and bool type). In C they would be equal.
- 0 is not equal to nil. (number type and nil type). In C they would be equal.
Regarding tables and C-made objects, they are compared by reference.
a = { 5 }
b = { 5 }
c = a
assert(a == c) -- OK
assert(a == b) -- ERROR
Boolean logic
Anything that is neither false nor nil is evaluated to true. This is pretty simple . Especially, an empty string or the zero number are evaluated to true.
local x = 0
if x then print("hello world") end -- prints hello world
Now regarding the and/or operators, two few rules to remember:
- The right operand is only evaluated when needed.
false and x()
does not call x() since the result will be false anyway.true or x()
does not call x() since the result will be true anyway.
- The result is the last operand to have been evaluated.
x() and y()
returns x() if it was false, y() otherwise.x() or y()
returns x() if it was true, y() otherwise.
Ternary statement and coalescence operator
We can exploit the and/or operators behavior:
x or y
returns x if x is neither nil nor false, y otherwise.
- In other words
result = x or y
is equivalent toif x then result = x else result = y end
.
condition and x or y
behaves like a ternary statement (as long as x is not nil or false): it returns x if condition was true, y if it was false.
- Indeed the expression becomes:
(condition and x) or y
. If condition is true the and operator returns x, and since x is true, y is not evaluated and or returns x. - In other words
result = condition and x or y
is equivalent toif condition then result = x else result = y end
.
Arrays versus tables
Ipairs versus pairs
Closures
Iterators
Coroutines
No integer type
Lua offers no support for integers and only has a 64-bits floating point number type. While this may cause troubles, you should not be overly afraid of this. For example, does the following assertion surprise you at least a bit: "any integer smaller than 2^52 can be accurately represented with a 64-bits floating point without any error"? If it did, you do not understand floating points well and you should better read carefully what's coming.
Many developers are confusing rounding errors with representation errors.
- Rounding errors actually only happen if you need a very high number of digits to represent a number. For example if you are adding a very large number with a small number. Rounding errors are very rarely encountered in day to day to applications. Actually most developers work their whole career without ever encountering them.
- Representation errors are the real problem: 0.1 is expressed in a decimal basis, but it has no finite representation in binary: it requires an infinite number of binary digits. This means that if you write 0.1 in your code it will be translated to a binary number close enough from 0.1 bot not exactly equal to it. So if you sum ten times 0.1, you sill sum ten times something close from 0.1 and the result will not be 1.
So the real problem for most developers only lies in the decimal-binary conversion that arises when you write literal numbers in your code that do not have a finite representation in binary. As long as you do not use such numbers, there will not be any problem. And here is the good point: integers do not suffer from any representation problem. Any integer can be represented in binary with a finite number of digits! This is why the absence of an integer type is typically not a source of problems.
Here a few examples to illustrate that point.
-- This loop works as intended: no representation error and the upper bound is far below the precision limit.
local sum = 0
for i = 0, 10000000000, 1 do sum = sum + 1 end
assert(sum == 10000000001)
-- This loop works as intended: 1/16 is a floating-point with an exact representation in binary
local sum = 0
for i = 0, 1, 1/16 do sum = sum + 1 end
assert(sum == 17/16)
-- This loop does NOT work as intended: 0.1 does not have an exact representation in binary
local sum = 0
for i = 0, 1, 0.1 do sum = sum + 1 end
assert(sum == 1.1) -- throws an error