Rsw:Help:Editing/Lua guide
Template:Construction Template:SE guide
Lua is a scripting language which is able to be used on the wiki. It allows more complex templates to be added than normal wikitext, and is usually easier to read than wikitext templates. Lua is implemented via an extension called Scribunto.
Important
The primary lua reference guide for this is Extension:Scribunto/Lua reference manual on MediaWiki which gives a short description of how to use every function in the standard libraries. While this guide will cover some of this, it will focus more on how to use lua as a whole rather than explaining the use of every available function. For some more explanation on the standard library functions see RuneScape:Lua/Library functions.
Editor
Since Scribunto uses the ace editor when editing lua modules, it offers some features to help you edit efficiently. The most notetable of those are the autocomplete function, line up or down movement/duplication, move to/select next/previous occurrence, toggling comments on or off over multiple lines at once and multiline editing. The autocomplete window can be opened by pressing ctrl+space, one can then scroll up or down using the mouse or arrow keys and select the desired word or snippet by clicking with the mouse or using the tab key. Multiline editing can be achieved by holding ctrl+alt and clicking in multiple locations or dragging the mouse. It also offers a range of keybinds which are listed below.
Keybinds
Line operations
Key combination | Explanation |
---|
Selection
Key combination | Explanation |
---|
Multicursor
Key combination | Explanation |
---|
Navigation
Key combination | Explanation |
---|
Find/Replace
Key combination | Explanation |
---|
Folding
Key combination | Explanation |
---|
Other
Key combination | Explanation |
---|
Debug console
Every module page has a debug console, which you can see by scrolling down past the editor. The console has access to the module (including unsaved changes) as variable p
. If possible, it is a good idea to test things here before saving. At the very least, you can input =p
and press enter; if you get anything other than 'table' output, you have things to fix. It is also a good place to test things like patterns and refresh yourself on simple facts. You can use mw.log()
to print strings and mw.logObject()
to dump tables to the console, if necessary.
Most of the examples given in his guide can be tested by copying their code into the console. Use shift+enter to write multiline code before submitting.
See the Frames section for information on using the debug console with modules that use the frame (generally in the form local args = frame:getParent().args
).
Lua basics
First module
All lua modules have to be written in the Module namespace - they cannot work if elsewhere. You can write test modules as a subpage of Module:Sandbox, i.e. Module:Sandbox/User:USERNAME (for example Module:Sandbox/User:<insert name here>
). You can have additional subpages of this if you want, like your userpage (e.g. Module:Sandbox/User:<insert name here>/example
).
All modules follow a basic pattern: declare and initialise a master variable, assign functions to that variable, return the variable. By convention, the master variable is named p
.
-- <nowiki>
local p = {}
function p.hello( frame )
return 'Hello, world!'
end
return p
-- </nowiki>
This code first declares the master variable p
using keyword local
, and initialises it to an empty table {}
. It then assigns a function named hello
to p
, with argument frame
which simply returns the string Hello, world!. p
is then returned. The Template:Tag comments prevent Mediawiki from parsing the module as wikicode and creating rogue links, it has no effect on the function of the module itself and it's considered best practice to add them.
If you save this module to Module:Sandbox/User:USERNAME, you can then put {{#invoke:Sandbox/User:USERNAME|hello}}
on any non-module page (e.g. your user space) to see the result (i.e. print Hello, world!).
This should be a reasonable demonstration on a simple module; most parts of this will be covered more later.
Basic syntax
Some lexical conventions
Identifiers (or names) in Lua can be any string of letters, digits, and underscores, not beginning with a digit. (e.g. i, j, a10, _i, longerNames)
You should avoid identifiers starting with an underscore followed by one or more upper-case letters (e.g., _VERSION); they are reserved for special uses in Lua. Usually, the identifier _ (a single underscore) is used for dummy variables, and identifiers starting with a _ followed by something else (e.g. _hello) is used for internal use of a package.
The following words are reserved; we cannot use them as identifiers:
and break do else elseif end false for function if in local nil not or repeat return then true until while
Lua is case-sensitive: and
is a reserved word, but And
and AND
are two different identifiers.
Lua needs no separator between consecutive statements, but we can use a semicolon if we wish. Line breaks play no role in Lua's syntax; for instance, the following four chunks are all valid and equivalent:
a = 1
b = a * 2
a = 1;
b = a * 2;
a = 1; b = a * 2
a = 1 b = a * 2 -- ugly, but valid
You can assign values to multiple variables in one line using a comma separated list, this will be useful later for functions who return more than one variable:
a, b, c = 1, 2, 3 -- a == 1, b == 2, c == 3
Assignment expressions are evaluated before assigning, so a, b = b, a
swaps the variable values.
Comments
Lua comments come in two types: single-line and multi-line. Anything written in a comment is not executed as code.
Single-line comments begin with --
and last until the end of the line.
Multi-line comments begin with --[=[
and end with ]=]
. Any number of equals signs can be between the brackets, including none, but the start and end need to have the same amount of equal signs.
Data types
To determine a data type of a variable, the type( var )
function can be used; it will return the type as a string (e.g. 'nil').
nil
nil
is the absence of value. You can't use it as a table key. When converted to a string, it is 'nil'; when converted to a number, it is still nil; when converted to a boolean, it is considered false - but remember, nil is not equal to false.
nil crops up in all sorts of places, so you'll need to remember to nil-check code. This is usually just a simple if statement:
if variable then
-- code if not nil
else
-- code if nil
end
or, if variable could legitimately be false:
if variable ~= nil then
-- code if not nil
else
-- code if nil
end
boolean
Boolean values are true
and false
. Their string representations are predictably 'true' and 'false'.
Notably, unlike some other languages, only false
and nil
are false values; the number 0 and the empty string are considered true.
string
Strings are a series of characters.
Literal strings - strings in the code directly - are enclosed by either '
(apostrophes/single quotes) or "
(double quotes). There is no difference between the two, though '
is preferred, unless actual apostrophes are used in the string. String literals have some escape sequences, notably:
\t
tab\n
newline\'
quote\"
double quote\\
backslash
String literals can also be assigned as 'long strings' using the same bracket notation of comments (long strings do not interpret escape sequences):
-- This long string
foo = [=[
bar\tbaz
]=]
-- is equivalent to this quote-delimited string
bar = 'bar\\tbaz\n'
Strings have a few functions associated with them, covered later.
Any other data type can be converted to a string using the tostring( value )
function. However, functions and tables simply return 'function' or 'table'[1], respectively.
We can get the length of a string using the length operator (denoted by #):
local a = 'hello'
mw.log( #a ) --> 5
mw.log( #'good bye' ) --> 8
Strings are immutable values, any operation that would change the value of the string returns an entirely new string with is operands unchanged.
You can compare strings with the ==
operator:
mw.log( 'hello' == 'hello' ) --> true
number
Numbers are any numerical object - there is no 'int' and 'float' types, just number. They're internally represented as 64-bit floating point numbers ('double' in many other languages). Integer and decimal parts are separated with a period - 1234.56
. Alternatively, E-notation can be used - 123.45e10
.
NaN and infinities are handled correctly, but there are no literals available for them. If necessary, 0/0
generates a NaN, 1/0
generates positive infinity, and -1/0
generates negative infinity.
Other data-types can be converted to a number using the tonumber( value )
function. However, this is only really meaningful for string values - other data-types, and strings that can't be converted to a number, return nil.
table
Tables are associative arrays, similar to JavaScript objects, PHP arrays, Perl hashes and somewhat to Java HashMaps. This means items are stored as key-value pairs
Tables in Lua are neither values nor variables; they are objects. You may think of a table as a dynamically-allocated object; programs manipulate only references (or pointers) to them. Lua never does hidden copies or creation of new tables behind the scenes.
Tables are created with curly braces - the empty table is {}
. Upon creation, tables can be filled using the following formats, separated by commas:
[expression1] = expression2
- the result of expression1 is the key and the result of expression2 is the value- Expression1 can be almost anything - a string with any characters, a number, a function, true/false, another table, etc.
name = expression
- equivalent to['name'] = expression
- Note that this method only works if name is a simple string without characters that may otherwise be interpreted - essentially, if it contains anything other than alphanumeric characters (plus underscore), you'll need to use type 1
expression
- equivalent to[auto incrementing number] = expression
- This notation is used to initialise tables with array like data in it. The auto incrementing number starts at 1.
Storing under a number is separate to storing under the string representation of that number. You cannot store under nil
.
The value stored can be of any type. Storing the value nil is equivalent to removing the key from the table.
For example:
local t = {
a = 'value for key a',
['b'] = 'value for key b',
['c'] = variable_used_elsewhere,
['9'] = 'value for key string 9',
[9] = 'value for key number 9',
[true] = {'a sub-table stored under the value for boolean true'},
[tonumber] = 'value for function tonumber',
[tostring] = function_stored_under_tostring,
}
You can also treat the table as a sequence of expressions (a traditional array):
local t = {
'a',
'b',
'c',
'd',
'e',
}
-- equivalent to
local t2 = {
[1] = 'a',
[2] = 'b',
[3] = 'c',
[4] = 'd',
[5] = 'e',
}
You can mix these two methods, but it is not advised for code clarity and the auto incrementing keys will overwrite manually defined keys. The length operator '#' will return the length of the array assuming there are not gaps in the number. If there is a gap its output is inconsistent; you will have to iterate over the entire table with pairs
to check for the highest index:
local t = {
[1] = 'a',
'b'
}
mw.log( t[1] ) --> 'b'
local t2 = {
'a',
'b',
'c',
[5] = 'd'
}
mw.log( #t2 ) --> 3 or 5
After creation, you can retrieve and assign to table values in two ways:
- If the key is a simple string, you can use dot notation:
t.a
- In all cases you can use bracket notation:
t['a']
Bracket notation is how you'd access an index stored in a variable, e.g.:
local t = {
a = 'value for key a',
['b'] = 'value for key b',
}
mw.log( t['a'] ) --> 'value for key a'
mw.log( t.a ) --> 'value for key a'
local index = 'a'
mw.log( t[index] ) --> 'value for key a'
t[index] = 'new value'
mw.log( t[index] ) --> 'new value'
You can mix array like keys with normal key-value pairs. The length operator '#' will only return the length of the array part.
local t = {
a = 'value for key a',
['b'] = 'value for key b',
['c'] = variable_used_elsewhere,
['9'] = 'value for key string 9',
'a',
'b',
'c'
}
mw.log( #t ) --> 3
mw.log( t['a'] ) --> 'value for key a'
mw.log( t[1] ) --> 'a'
You can append a value to the end of an array using table.insert
:
local t = {
'a',
'b',
}
table.insert( t, 'c' )
mw.log( t[3] ) --> 'c'
While debugging, you can use mw.logObject( table )
to print out he whole table in a readable format.
local t = {
a = 'value for key a',
['b'] = 'value for key b',
['9'] = 'value for key string 9',
[true] = {'a sub-table stored under the value for boolean true'},
'a',
'b',
'c'
}
mw.log( t ) --> 'table'
mw.logObject( t ) --[=[
table#1 {
'a',
'b',
'c',
[true] = table#2 {
'a sub-table stored under the value for boolean true',
},
['9'] = 'value for key string 9',
['a'] = 'value for key a',
['b'] = 'value for key b',
}
]=]
Tables are stored by reference in variables. This means that a variable only stores where in memory a table is located; it doesn't store all the data of the table. As a result multiple variables can point to the same table and a table is only really removed from memory when all variables linking to the table are destroyed. You can create a real copy of a table using the mw.clone()
function.
local t1 = { 1 }
local t2 = t1
local t3 = mw.clone( t1 )
t1[1] = 2
mw.log( t1[1] ) --> 2
mw.log( t2[1] ) --> 2
mw.log( t3[1] ) --> 1
t1 = nil
mw.log( t2[1] ) --> 2
t2 = nil -- Now the table is really destroyed, the cloned table t3 still exists
Numbered or string keys
Numbered keys, or sequences, are used when the order of your data is important but the exact location of it in the sequence is not. For example if you want to store a sentence word by word, it is important that the order of the words is maintained but we don't care which word is at a specific index:
local t = { 'This', 'is', 'a', 'sentence', '.' }
local t2 = { 'This', 'is', 'a', 'longer', 'sentence', '.' }
In the above example it is important that the word This
is before is
, but we don't care that the word sentence
is at index 4 or 5.
String keys are used when the location of data is important. For example, when we want to store certain properties about an item:
local ashes = {
tradeable = true,
equipable = false,
stackable = false,
value = 2,
['buy limit'] = 10000
}
function
Functions can be treated as normal variables - assigned, overwritten, passed as arguments, created anonymously, etc.
Functions are created with function
, and are covered more later.
Structures
Operators
The following operators are supported:
Arithmetic
+
addition-
subtraction (or negation)*
multiplication/
division%
modulo^
exponentiation
When attempting to do arithmetic on a string, Lua will try to convert the strings to numbers with the tonumber()
function; it is still advised to call the tonumber()
function explicitly for code clarity.
mw.log( 5 + '10' ) --> 15
Relational
==
equality~=
non-equality>
greater than<
less than>=
greater than or equals<=
less than or equals
Logical
and
or
not
and
and or
are short-circuit - foo() or bar()
will only call bar()
if foo()
is false or nil.
Lua supports a conventional set of logical operators: and
, or
, and not
. Like control structures, all logical operators consider both the Boolean false
and nil
as false, and anything else as true. The result of the and
operator is its first operand if that operand is false; otherwise, the result is its second operand. The result of the or
operator is its first operand if it is not false; otherwise, the result is its second operand:
mw.log( true and true ) --> true
mw.log( true and false ) --> false
mw.log( true and nil ) --> nil
mw.log( nil and true ) --> nil
mw.log( true and 4 ) --> 4
mw.log( 5 and 4 ) --> 4
mw.log( nil and 4 ) --> nil
mw.log( nil or 4 ) --> 4
mw.log( 4 or nil ) --> 4
mw.log( 4 or 5 ) --> 4
This is very useful to set default values in case a variable is nil.
local var = arg or 0 -- var == arg if arg is not nil, otherwise var == 0
local var = arg and math.pow( arg ) or 0 -- prevents 'math.pow' from throwing an error and store 0 in case arg == nil
Concatenation
To join two strings together use ..
. When trying to concatenate a number and a string Lua will automatically convert the number to a string with the tostring()
function.
mw.log( 'Foo' .. 'Bar' ) --> 'FooBar'
local var1 = 'Foo'
local var2 = 'Bar'
mw.log( var1 .. var2 ) --> 'FooBar'
mw.log( 'Foo' .. 10 ) --> 'Foo10'
If multiple concatenations are required, it may be faster and easier to use string.format()
or table.concat()
(see later).
Length
The length operator is #
, used #a
. If a is a string, the length of the string in bytes is returned. If a is a sequence, returns the number of entries in the array.
If a is table but not a strict sequence, it returns a value n such that a[n] is not nil and a[n+1] is nil - but this may not be consistent, so avoid using this operator on non-sequence tables.
Precedence
Order of operations is:
- ^
- not # - (negation)
- * / %
- + - (subtraction)
- .. (concatenation)
- < > <= >= ~= ==
- and
- or
Functions
The basic function is:
local function ( variable_list )
--block of code
end
Functions can be given names in two ways:
- by assigning it to a variable:
do_this_thing = function ( variables ) ...
- by putting the name just before the variable list:
function do_this_thing( variables ) ...
These also apply to tables:
t.do_this_thing = function ( variables ) ...
function t.do_this_thing( variables ) ...
Newly declared variables in a function should always be local (more on that later). Variables declared outside the function can be used within it. Functions can be declared within other functions.
Values are returned from the function with the return
keyword.
local function addOne( num )
local numplus1 = num + 1
return numplus1
end
mw.log( addOne( 10 ) ) --> 11
Functions can return comma-separated lists that aren't inside an array.
local function addOneTwoThree( num )
return num + 1, num + 2, num + 3
end
local a, b, c = addOneTwoThree( 10 ) --> a == 11, b == 12, c == 13
local d = addOneTwoThree( 10 ) --> d == 11, other two values thrown away
local _, e = addOneTwoThree( 10 ) --> e == 12, other two values thrown away, using the _ as a placeholder
When used in control structures (e.g. if addOneTwoThree( 0 ) then ... end
), only the first value is used.
When a function f returns the result of another function g which returns more than one result, but we are only interested in the first result of g we can wrap it in parenteses:
local function g()
return 1, 2
end
local function f()
return ( g() )
end
local a, b = f() --> a == 1, b == nil
It is allowed to supply more or less arguments than the function accepts:
local function add( num, num2 )
num = num or 0
num2 = num2 or 0
return num + num2
end
mw.log( add( 5 ) ) --> 5; num2 == nil
mw.log( add( 1, 2, 4 ) ) --> 3; 4 is ignored
Functions are stored by reference and can be passed to other variables, tables and function arguments.
local function addOne( num )
return num + 1
end
local a = addOne
mw.log( a( 10 ) ) --> 11
local t = {
foo = addOne
}
mw.log( t.foo( 10 ) ) --> 11
local function exec( func, num )
return func( num )
end
mw.log( exec( addOne, 10 ) ) --> 11
A Lua file is executed from top to bottom, therefore functions must declared before they are used:
mw.log( addOne( 10 ) ) --> Error
local function addOne( num )
return num + 1
end
mw.log( addOne( 10 ) ) --> 11
It is possible to use a local functions inside another function before it is declared using forward declaration:
local f -- Forward declaration
local function addOne( num )
return num + f()
end
function f() -- No local keyword here since 'f' is already declared local higher up
return 1
end
mw.log( addOne( 10 ) ) --> 11
Global functions are implicitly forward declared.
You can create recursive local functions.
-- This
local function factorial( num )
if num <= 1 then
return 1
else
return num * factorial( num - 1 )
end
end
-- Is a syntactic sugar for
local factorial
factorial = function( num )
if num <= 1 then
return 1
else
return num * factorial( num - 1 )
end
end
-- This does not work
local factorial = function( num )
if num <= 1 then
return 1
else
return num * factorial( num - 1 ) -- The variable 'factorial' is not yet defined so it tries to call the global variable instead of the local one
end
end
If a function is called with a single table as argument, it is possible to slightly shorten its notation by using {}
instead of ()
:
local function sum( t )
...
end
sum( { name = value } )
sum{ name = value }
The output of one function can be used as the input of another. Take for example the following code:
local function foo( num )
mw.log( 'foo' )
return num
end
local function bar( num )
mw.log( 'bar' )
return num * 2
end
local result = bar( foo( 10 ) ) --> result == 20
--[=[ Prints: 'foo'
'bar'
]=]
The mw.log statements allow us to track which function is executed first. Looking at the printed text, it is clear that first the function foo
is executed, printing its name and returning the value 10. This value is then passed into function bar
which prints its own name and returns the value 20, which is then stored in the variable result
. So result
only gets to see the output of the very outer function and never gets to see anything that happened before the bar
function ended.
The standard libraries are just functions stored in tables, e.g. math.sin()
means, in table math
executed the function stored at key sin
.
Lua also offers a special syntax for object-oriented calls, the colon operator. An expression like o:foo()
calls the method foo
in the object o
. (more on this later)
Varargs
Varargs (variable arguments) can be used by functions that should work on any number of passed arguments, but the number is not known ahead of time. They are represented by using ...
in the function:
local function sum( ... )
local t = { ... }
local s = 0
for i, v in ipairs( t ) do -- More on this later
s = s + v
end
return s
end
mw.log( sum( 4, 5, 6, 7, 8, 9 ) ) --> 39
In the function declaration, ...
has to be the last 'parameter'. Additional parameters can come first, though. Inside the function, the list of varargs is referenced by ...
. It is common, as in the example above, to turn the varargs into a sequence by wrapping it in curly braces. Then it can be iterated over by ipairs.
Control structures
Control structures come in two main flavours: conditional and loops.
if
If statements are of the generic form:
if expression1 then
--block if expression1 is true
elseif expression2 then
--block if expression1 is false and expression2 is true
else
--block if all expressions are false
end
The elseif and else sections can be omitted if they are not needed.
Each expression should result in a boolean, but remember that all values can evaluate to a boolean: false and nil are false, and all other values are true (including 0, the empty string, and the empty table).
e.g.:
local var = 1
if var == 0 then
mw.log( 'if' )
elseif var > 10 then
mw.log( 'elseif' )
else
mw.log( 'else' )
end
--> 'else'
while
While loops repeat a given block until an expression returns false.
while expression do
--block to evaluate while expression is true
end
e.g.:
local i = 10
local fib = 1
while i > 0 do
fib = fib + fib
i = i - 1 -- Very important, without this the loop would go on forever
end
mw.log( fib ) --> 1024
repeat
Repeat is essentially the reverse of a while loop - it repeats the block until the expression is true.
repeat
--block to evaluate while expression is false
until expression
for
For loops repeat a number of times. There are two forms of for - the second of which is under the for-each heading below.
for name = start, stop, step do
--block to evaluate
end
This version declares the local variable name
, assigns it value start
, then iterates the block, adding step
to name
each iteration until name
exceeds stop
. All three values can be expressions that evaluate to numbers.
step
may be omitted to use the default value of 1.
For name
the values i
and j
are commonly used.
e.g.:
local var = 0
for i = 10, 0, -1 do
var = var + i
end
mw.log( var ) --> 55
for-each
For each loops are primarily for table iteration. They require iterator functions, which are handily provided by the pairs
and ipairs
functions. You can use custom functions, but that is beyond the scope of this guide.
for k, v in pairs( t ) do
--block to evaluate
end
This loop then iterates over the block, setting k
to the key and v
to the associated value for each entry in the table.
The pairs()
function provides iteration over all values in the tables. The order is not reliable or consistent, so if order is important you may need another way.
The ipairs()
function provides iteration over sequences. It will always iterate in the order of the sequence, if the sequence contains gaps it will stop at the first gap. All non number keys are ignored.
To order a non-sequence, you can use the opairs
function of Module:Iterator. If you want a more controlled order, you can set a manual order in a sequence (or iterate over the table a few times, first to get the keys/values in a sequence and order them with table.sort
, then to iterate in the order).
e.g.:
local t = {
a = 'a',
b = 'b',
'c',
'd'
}
for k, v in pairs( t ) do
mw.log( 'key: ' .. k .. '; value: ' .. v )
end
for i, v in ipairs( t ) do
mw.log( 'index: ' .. i .. '; value: ' .. v )
end
--[[
key: 1; value: c
key: 2; value: d
key: a; value: a
key: b; value: b
index: 1; value: c
index: 2; value: d
]]
Scoping
In computer programming, the scope of a name binding—an association of a name to an entity, such as a variable—is the region of a computer program where the binding is valid: where the name can be used to refer to the entity. Such a region is referred to as a scope block. In other parts of the program the name may refer to a different entity (it may have a different binding), or to nothing at all (it may be unbound).[2]
In short, scope is the concept where only parts of your program have access to certain variables. e.g.:
for i = 1, 10 do
...
end
mw.log( i ) --> nil, we don't have access to the variable 'i' outside the for loop
With scoping it is possible to re-use the same variable name in different scope blocks; this is not recommended for code clarity but it explains the concept well.
local var = 'top level'
if true then
mw.log( var ) --> 'top level', we can access variables defined in a higher level scope
local var = 'level 1' -- This variable is restricted to the scope of the first if statement, the top level variable is unchanged but we can no longer access it from within this scope
mw.log( var ) --> 'level 1'
if true then
mw.log( var ) --> 'level 1'
local var = 'level 2'
mw.log( var ) --> 'level 2'
end
mw.log( var ) --> 'level 1'
end
mw.log( var ) --> 'top level'
All control structures (e.g. if, for, ...) and functions create a new scope.
Global or local
Global variables do not need declarations; we simply use them. It is not an error to access a non-initialized variable; we just get the value nil as the result:
mw.log( b ) --> nil
b = 10
mw.log( b ) --> 10
If we assign nil to a global variable, Lua behaves as if we have never used the variable:
b = nil
mw.log( b ) --> nil
Lua does not differentiate a non-initialized variable from one that we assigned nil. After the assignment, Lua can eventually reclaim the memory used by the variable.
Global variables are stored in the _G
table. Global variables always have global scope; this means once it is created every scope has access to it until it is manually deleted:
mw.log( var ) --> nil
local function test()
var = 'level 1' -- Global variable
end
test()
mw.log( var ) --> 'level 1'
We can localise a variable to a given scope with the local
keyword. Doing so has a few advantages:
- Speed. It is faster to access a local variable.
- Memory saving. Once we exit the scope in which the local variable was created it is automatically destroyed.
- Preventing naming collisions or unexpected behaviour.
function main()
a = 1 -- A global variable
local b = 2 -- A local variable
test()
mw.log( a ) --> 10
mw.log( b ) --> 2
end
function test()
a = 10 -- Also a global variable
local b = 20
end
The above example shows the problem with global variables. In both functions the variable a
points to the same global variable, so the function test
unintentionally changes the internal variable of the function main
. For this reason you should always localise your variables.
It is also a good idea to localize your functions to prevent you from accidentally overwriting a function with the same name inside a module you require, demonstrated by the following example:
- Module:Foo
-- <nowiki>
local p = {}
function printName() -- Global function
mw.log( 'Foo' )
end
local function printName2() -- Local function, this means it can only be used inside this module
mw.log( 'Foo' )
end
function p.name()
printName()
end
function p.name2()
printName2()
end
return p
-- </nowiki>
- Module:Bar
-- <nowiki>
local foo = require( 'Module:Foo' ) -- More about this later
local p = {}
function printName() -- Global function
mw.log( 'Bar' )
end
local function printName2() -- Local function
mw.log( 'Bar' )
end
function p.main()
printName() --> 'Bar'
printName2() --> 'Bar'
foo.name() --> 'Bar'
foo.name2() --> 'Foo'
end
return p
-- </nowiki>
The function printName
in Module:Foo
is overwritten by the function in Module:Bar
.
Scribunto
In Scribunto modules are called using the #invoke parser function with the following syntax: {{#invoke:<module name>|<function to call>|<arg>}}
<module name>
- name of the module without theModule:
namespace prefix.<function to call>
- the function you want to call must be inside a table that is returned by the module; usually this table is calledp
. Functions outside this table can't be accessed by #invoke.<args>
- same style of argument you can pass to templates. This is optional.
Frames
So far, the above has been about core lua. Now, we need to consider how this applies to the wiki - the most important of which is how modules are called.
Think of this as some sort of layer cake. Imagine the following example template and module are real and called from this page (you can test it yourself by copying the example to your userspace and sandbox module; see #First_module):
{{Mytemplate|text|3|separator=;}}
: text;text;text;suffix
- Template:Mytemplate
{{#invoke:Mymodule|main|last=suffix}}
- Module:Mymodule
-- <nowiki>
local p = {}
function p.main( frame )
local invokeArgs = frame.args
local args = frame:getParent().args -- Template args are customary stored in the 'args' variable
mw.log( args[1] ) --> 'text'
mw.log( args[2] ) --> '3'
mw.log( args.separator ) --> ';'
mw.log( invokeArgs.last ) --> 'suffix'
local text = args[1] or '' -- Set default value in case no parameters were passed to the template
local multiplier = tonumber( args[2] ) or 0
local separator = args.separator or ''
local last = invokeArgs.last or ''
local res = ''
for i = 1, multiplier do
res = res .. text .. separator
end
return res .. last
end
return p
-- </nowiki>
- Right here, this page calls Template:Mytemplate with parameter
1
set totext
, parameter2
set to3
and parameterseparator
set to;
. - This then loads up Template:Mytemplate and passes the parameters. In a normal wikicode template, this is where it does parsing - possibly with other templates helping - and returns up to the page. But, as Mytemplate uses #invoke, it moves down to the lua layer.
- The #invoke called Module:Mymodule's
main
function, with an additional parameter oflast
set tosuffix
. So, it executes the functionp.main
. Once it has the result, it passes it back up to the template, and the template passes it up to the page.
Now, how does Module:Mymodule get both sets of parameters? This is from the main variable passed to the function, the frame. Frames are tables with a few functions and values.
- The #invoke call generates a frame for itself, with the notable part being the
args
value. Since this #invoke specifies a parameter, it getsargs = {last = 'suffix'}
. - The #invoke also generates a frame for the template call, which has the
args
value set toargs = { [1] = 'text', [2] = '3', separator = ';' }
. This frame is set as a parent to the previous frame. - The
main
function in Module:Mymodule is passed the child frame. The parent can be accessed using thegetParent()
function.
Thus, in this example:
frame.args
={ last = 'suffix' }
frame:getParent().args
={ [1] = 'text', [2] = '3', separator = ';' }
- Notes
- Unnamed template parameters are passed as a sequence.
- Argument values are always strings.
getParent()
will return nil if there is no parent - e.g. if #invoke is directly used, or in the debug console.
- Debug console
When we call the main function from the debug console, we would have to create our own dummy frame. To make it easier to use the debug console it is customary to split the bulk of the code from the frame using a _main function. e.g.:
-- <nowiki>
local p = {}
function p.main( frame )
local invokeArgs = frame.args
local args = frame:getParent().args
return p._main( invokeArgs, args )
end
function p._main( invokeArgs, args )
local text = args[1] or ''
local multiplier = tonumber( args[2] ) or 0
local separator = args.separator or ''
local last = invokeArgs.last or ''
local res = ''
for i = 1, multiplier do
res = res .. text .. separator
end
return res .. last
end
return p
-- </nowiki>
If you need to test a function that does not use the above method you can create a frame object in the console. Copy the following code into the console, replacing the -- Args go here as ['name'] = value,
with appropriate args and the p.main
call with the correct function name:
local frame = {}
function frame.getParent()
local args = {
-- Args go here as ['name'] = value,
}
return { args = args }
end
mw.log( p.main(frame) )
Importing
There are a number of modules already written which provide functionality you may need. There are two ways of accessing this.
Requiring modules
The core function require
is the general-purpose module importer. To require a module, simply use the full page name of the module you need as the only parameter. This returns whatever the module returns (usually the p
table, but some modules return a single function). You can then use the functions/data of the module as normal.
-- import [[Module:Paramtest]]
local paramtest = require( 'Module:Paramtest' )
-- use the has_content function of Paramtest
paramtest.has_content( args.foo )
If you only need to use one function from the module, you can assign that function to a local variable:
local hc = require( 'Module:Paramtest' ).has_content
hc( args.foo )
You can import any module, but ones designed with common functions to be imported are called 'helper modules'; you can see a list of these at RuneScape:Lua/Helper modules.
Loading data
If you're loading data - not functions - it is more efficient to use the Scribunto function mw.loadData
. This functions identically to require
, except for a few conditions:
- The module is evaluated once per page load, instead of once per #invoke (thus making it more efficient when repeatedly used on a page)
- The module must return a table, and that table (and all subtables) can only contain numbers, strings, booleans, or more tables - notably, functions are not allowed. The keys of all tables must be booleans, numbers, or strings.
- The table returned is read-only
local data = mw.loadData( 'Module:Experience/elitedata' )
Data is usually stored as a subpage of the primary module. Some examples of data pages:
- Module:Experience/elitedata is a simple list
- Module:Globals/data is a table of tables
- Module:Experience/data generates all the values from the level-experience formula
- Module:Disassemble/data defines a main data table, then does additional parsing on that table to create the actual return value
If you're using data, you should absolutely try to use mw.loadData
over require
or putting it into the module directly. The only exception is when you need to access this data a lot in a tight loop because a side effect of making the table read only is that its access time significantly increases (times 12 if reading a key that exists, times 6 otherwise).
Documentation
Documentation of a module can be edited on the /doc
subpage of the module. It is good practice to always add a doc page with at least {{Documentation}}
or {{No documentation}}
on it. Those templates will then generate a dependency list based on the content of your module which makes it a lot easier to find out which modules rely on each other.
See some examples of this:
Advanced lua
Tail calls
A feature of functions in Lua is that Lua does tail-call elimination. A tail call is a goto dressed as a call. A tail call happens when a function calls another as its last action, so it has nothing else to do. For instance, in the following code, the call to g
is a tail call:
function f( x )
x = x + 1
return g( x )
end
After f calls g, it has nothing else to do. In such situations, the program does not need to return to the calling function when the called function ends. Therefore, after the tail call, the program does not need to keep any information about the calling function on the stack. When g returns, control can return directly to the point that called f. Some language implementations, such as the Lua interpreter, take advantage of this fact and actually do not use any extra stack space when doing a tail call. We say that these implementations do tail-call elimination.
Because tail calls use no stack space, the number of nested tail calls that a program can make is unlimited. For instance, we can call the following function passing any number as argument:
function foo( n )
if n > 0 then
return foo( n - 1 )
end
end
It will never overflow the stack.
A subtle point about tail-call elimination is what is a tail call. Some apparently obvious candidates fail the criterion that the calling function has nothing else to do after the call. For instance, in the following code, the call to g is not a tail call:
function f( x )
g( x )
end
The problem in this example is that, after calling g, f still has to discard any results from g before returning. Similarly, all the following calls fail the criterion:
return g( x ) + 1 -- must do the addition
return x or g( x ) -- must adjust to 1 result
return ( g( x ) ) -- must adjust to 1 result
In Lua, only a call with the form return func( args )
is a tail call. However, both func and its arguments can be complex expressions, because Lua evaluates them before the call. For instance, the next call is a tail call:
return x[i].foo( x[j] + a*b, i + j )
Tail calls use no stack space, this means Lua does also not keep any information about the name of the function they call or at which line the call happened. Therefor the info Lua can give in a stack traceback is limited, in fact the only info it can give is that a tail call happened and nothing more. For example take the following code:
local p = {}
function p.foo()
return p.bar() -- Tail call one
end
function p.bar()
return p.buz() -- Tail call two
end
function p.buz()
return 1 + nil -- This will throw an error
end
return p
If we call function foo using p.foo()
in the console, we get:
Lua error at line 12: attempt to perform arithmetic on a nil value. Backtrace: Module:Sandbox/User:USERNAME:12: ? (tail call): ? (tail call): ? console input:7: ? [C]: ?
In comparison when not using tail calls:
local p = {}
function p.foo()
p.bar()
end
function p.bar()
p.buz()
end
function p.buz()
return 1 + nil -- This will throw an error
end
return p
If we call function foo using p.foo()
in the console, we get:
Lua error at line 12: attempt to perform arithmetic on a nil value. Backtrace: Module:Sandbox/User:USERNAME:12: in function "buz" Module:Sandbox/User:USERNAME:8: in function "bar" Module:Sandbox/User:USERNAME:4: in function "foo" console input:7: ? [C]: ?
Closures
First-class values
Functions are first-class values in Lua. This means that functions can be stored and passed around like normal variables. On the hardware level this is achieved by storing the instructions of the function in memory at a known location and then storing this memory address in a variable. So when we then copy this variable or pass it as an argument to a function all we do is copy the address to the function, the content of the function is never moved. Lua will also keep track of all the variables that point to this function and only when all references are gone the function is deleted from memory. Now since Lua is a dynamically typed language (the type of a variable can change at runtime) this is a little bit more complex in reality as it also need to keep track of what data type this memory address points to, but the general idea still applies.
-- Create a nameless function in memory which returns the value 0.
-- Then store the memory address of this function in the local variable 'foo'.
-- If for example the function is located at address 0x00001000, then foo = (function pointer 0x00001000)
local function foo()
return 0
end
local bar = foo -- bar == (function pointer 0x00001000)
Lexical scoping
What does it mean for functions to have "lexical scoping"? It means that functions can access variables of their enclosing functions. The reason this is special is because functions are first-class variables which means that a function can escape the scope it was created in. So it also means that a function can bind/capture local variables of its enclosing scope to be used later even when the scope where the local variable was created no longer exists!
local names = {'Peter', 'Paul', 'Mary'}
local grades = {Mary = 10, Paul = 7, Peter = 8}
table.sort( names, function( n1, n2 )
return grades[n1] > grades[n2] -- compare the grades
end )
In the above example the anonymous function (also called a lambda) that is used for sorting has access to the variable grades even though it is never passed to the function as an argument.
Since functions can bind variables to itself it is possible to make functions that sort of behave like objects with their own local variables.
local function foo( num )
local n = num or 0
local function bar()
mw.log( n )
n = n + 1
end
return bar
end
local a = foo( 5 )
local b = foo( 10 )
a() --> 5
b() --> 10
a() --> 6
b() --> 11
The function bar binds the variable n to itself, and each instance of bar gets its own version of n. We call bar a closure. The function foo does not access any outer variables so it is not a closure but just a normal function. Creating a closure is slightly more expensive than a normal function but in most cases you don't need to worry about it.
Generic for
By now you are probably familiar with the pairs and ipairs functions to iterate over a table with a for-loop. Now Lua allows you to write your own functions to iterate over using a for-loop (Module:Iterator). The syntax for this generic for is as follows:
for var_list in _f, _t, _var do
...
end
var_list is a list of one or more comma separated values. _f is a function that is called with each iteration. _t is the first argument to _f, usually this is a table we want to iterate over. _var is the second argument to _f, this is used to keep track of the current state. Usually we use a single function that returns the values for those three slots.
local myTable = {'a', 'b', 'c'}
local _f, _t, _var = ipairs( myTable ) -- _t == myTable, _var == 0
for i, v in _f, _t, _var do
mw.log( i, v )
end
-- 1 a
-- 2 b
-- 3 c
In general, a for-loop like
for var_1, ..., var_n in explist do
-- block
end
Is equivalent to
do
local _f, _t, _var = explist
while true do
local var_1, ..., var_n = _f( _t, _var )
_var = var_1
if _var == nil then
break
end
-- block
end
end
Stateless iterator
As the name implies, a stateless iterator is an iterator that does not keep any state by itself. Therefore, we can use the same stateless iterator in multiple loops, avoiding the cost of creating new closures.
As we just saw, the for-loop calls its iterator function with two arguments, one is the table we are iterating over and the other is a state variable. An example of this kind of iterator is ipairs. Now lets implement our own ipairs function.
local function iter( t, i )
i = i + 1
local v = t[i]
if v then
return i, v
end
end
function ipairs( t )
return iter, t, 0
end
Every iteration of the for-loop the iter function is called. The first time the value of the argument i is 0; the function then returns 1, t[1]
. The second loop i is 1 and the function returns 2, t[2]
. This continues untill the table t has run out of elements and returns nil; iter then also returns nil. This signals the for-loop that the iteration is done.
Closure as iterator
It is not uncommon that the iterator function needs more than one state variable and/or it needs a state variable that is not meant to be exposed to the outside. In this case we can use a closure as iterator function since as we saw before a closure can remember variables. Lets make another implementation of ipairs but using a closure this time.
function ipairs( t )
local i = 0
return function()
i = i + 1
local v = t[i]
if v then
return i, v
end
end
end
Since we are using a closure now the iterator function no longer needs any arguments and thus ipairs does not need to return a value for _t and _var. The biggest downside of this method is that we need to create a new closure for every for-loop that uses this ipairs implementation whereas the stateless iterator only has to create its iter function once.
Metatables
Object oriented programming
Performance optimizations
Function profiling
Semantic mediawiki
See also
- Wikipedia:Lua (programming language) and Wikipedia:Project:Lua, for a more in-depth breakdown of the coding Language
- Lua reference manual on MediaWiki, for the documentation of Lua as used by the Scribunto extension, including standard functions
- RuneScape:Lua/Helper modules a list of all our modules designed to help with writing other modules
- RuneScape:Lua/Library functions Examples for most standard functions, as well as our own helper modules (exchange, maps etc)
- RuneScape:Lua/Modules, for a list of all modules, excluding Exchange and miscellaneous data pages
- Category:Lua-based templates, for an index of templates that directly invoke Lua
- Special:PrefixIndex/Module:, for a list of all current modules
- Special:PrefixIndex/Module:Data/, for a list of modules containing data for the data namespace
- Special:PrefixIndex/Module:Sandbox/, for a list of personal test modules
Footnotes
- ^ The behaviour of the
tostring()
function on tables can be changed using the__tostring
metatable index. More on that later. - ^ https://en.wikipedia.org/wiki/Scope_(computer_science)