Lua introduction for confirmed developers: Difference between revisions

From Civilization Modding Wiki
Jump to navigationJump to search
mNo edit summary
No edit summary
Line 170: Line 170:
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:
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.
* '''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.
* '''number''': a 64 bits floating-point number. Lua has no support for integers and only use floating-points. See also [[#No integer type]].
* '''string''': your usual string of characters. In Lua all strings are interned. This speeds up comparisons but it makes allocation of new strings slower.
* '''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.
* '''boolean''': true or false obviously.
Line 254: Line 254:




== Advanced topics ==
== Semi-advanced topics ==
=== Closures ===
=== Closures ===
=== Iterators ===
=== Iterators ===
=== Coroutines ===
=== No integer type ===
=== 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.
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.
Line 290: Line 286:
assert(sum == 1.1) -- throws an error
assert(sum == 1.1) -- throws an error
</syntaxhighlight>
</syntaxhighlight>
== Advanced topics==
=== Metatables and metamethods ===
=== Implementing a type hierarchy ===


[[Category:Civ5 API]]
[[Category:Civ5 API]]

Revision as of 17:01, 20 September 2012

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          -- True only if a and b have the same values and types.
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.

local x = #"abc"                      -- Returns 3, the number of bytes in the string (3 characters, 1 byte each).
local x = #"您好世界"                  -- Returns 8, the number of bytes in the string (4 characters, 2 bytes each).
local x = #SomeTable                  -- Returns the biggest integer key in the table, or nil if there is not integer key.

-- 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)


-- Multiple return values
function func(a, b) return a, b end
local x, y = func(5, 6)        -- Assigns 5 to x and 6 to y


-- 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. See also #No integer type.
  • 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.

if 0 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 and coalescence statements

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 to if 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 to if condition then result = x else result = y end.


Arrays versus tables

In Lua, an array is a table whose keys are integers.

Since this is a common programming pattern, a number of things are designed for arrays:

  • {"abc", "def", "ghi"} starts at index 1, up to index 3. Arrays typically start at 1.
  • table.insert({}, "abc") will transform the provided empty table to {"abc"}
  • table.insert({"abc", "def"}, "ghi") will transform the provided table to {"abc", "def", "ghi"}
  • table.insert({"abc", "ghi"}, 2, "def") will transform the provided table to {"abc", "def", "ghi"}
  • table.remove({"abc", "def", "ghi"}, 2) will transform the provided table to {"abc", ""ghi"}
  • #{"abc", "def", "ghi"} returns 3, the biggest integer key.


Ipairs versus pairs

You could say that pairs is for table and ipairs for arrays.

  • Pairs enumerate all the key/value pairs in an unordered matter.
  • Ipairs only enumerates the key/value pairs for consecutive integer keys, in an ascending order, starting from 1 up to the largest key. If at some point a key is missing, the enumeration stops.
It does not matter whether the table was crated with table.insert or directly by inserting the values through, for example table[i] = value.
-- Produces: { 1 => "one", 2 => "two", 4 => "four", "name" => "John", "age" => 25 }
local t = { "one", "two", name = "John", age = 25, [4] = "four" }

-- Will print (for example, the order is undetermined): "name", 2, 4, "age", 1
for k in pairs(t) do print(k) end

-- Will print 1, 2
for k in ipairs(t) do print(k) end

-- Both ipairs and pairs return a key and a value, so you could also use:
for k, v in pairs(t) do print(k, v) end


Semi-advanced topics

Closures

Iterators

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


Advanced topics

Metatables and metamethods

Implementing a type hierarchy