Lua Tutorial – Basics

I am creating this as a quick summary of all of the content in my Lua Tutorial to date. I am creating this so that I have a better point to jump off when I resume making more content for the Lua tutorial down the line. This document will be a review of everything I have covered to try and make it easier to jump back to necessary sections down the line.

Introduction

Lua (https://www.lua.org/) is a cool interpreted language which is typically known for being embedded in programs as a scripting language to extend programs, but it is also a fast, light, portable standalone language. The core language is extremely simple, but that doesn’t mean it can only do simple tasks. Lua supports most things modern languages do, and is extremely extensible. Library support is also pretty good with things like luarocks (https://luarocks.org/) which should be available from any Linux distribution’s package manager, and can easily be found online for download from a couple projects or from somewhere like http://luabinaries.sourceforge.net/. I won’t cover installation in this series since there are several different ways, and none are particularly difficult for most people.

For all of the full script examples I include in this series, I will be using the syntax you would use on a standard Linux or Mac OS X environment. This will basically amount to a one-line difference and shouldn’t impact the way anything is done in any script or run in any script as I am going to try and keep everything very standard and very clean where possible (I use the same convention on Windows which completely ignores it). This series will also mainly target 5.1, but everything should be compatible with newer versions. I chose 5.1 due to some of the libraries, support, and because it was the most universally stable in the environments I need to use it on. Lua stays backwards compatible for the most part, so what works in 5.1 will work in 5.3, so only newer features really need to be added.

Hello World

hello.lua:

#!/usr/bin/lua5.1

print( "Hello World!" )

If we run:
Windows:

lua51.exe hello.lua

*NIX:

chmod +x hello.lua; ./hello.lua

We should see:

./hello.lua 
Hello World!

One thing to keep in mind, since we are writing this on macOS and Linux, we are going to use the shebang (#!/usr/bin/lua5.1). The only real thing we care about for our entire program, is the print command. Print takes an argument or set of arguments and prints them onto the standard output for us. “Print” adds a newline by default.

We can also add to our strings or concatenate them with the “..” operator. This basically adds a string to another string.

hello2.lua:

#!/usr/bin/lua5.1

print( "1" .. "0" )

If we run this, we should see the following:

./hello2.lua
10

We could then edit this to do some basic math, if we write the following hello3.lua:

#!/usr/bin/lua5.1

print( 1 + 1 )

This will then print the string interpretation of 1 + 1 which is 2.
If we continue doing similar, we can also do something like follows for hello4.lua:

#!/usr/bin/lua5.1

print( 1 + 1 .. 0 + 0 )

The will give us the string interpretation 1 + 1 then append the string representation of 0 + 0 giving us “20”. We can continue with different math operations for hello5.lua:

#!/usr/bin/lua5.1

print( 1 + 1 .. 3 - 1 .. 2 * 1 .. 4 / 2 .. 5 % 3 )

This will display 1 + 1 = 2 appended to 3 – 1 = 2 appended to 2 * 1 = 2 appended to 4 / 2 = 2 appended to 5 % 3 = 2 (5 divided by 3 = 1 + remainder of 2 for the modulo operator) for “22222”.

Now, to review, and for readers looking to skip ahead through the explanations, we can put all of this together in a new hello_all.lua script:

#!/usr/bin/lua5.1

print( "Hello World!" )
print( "1" .. "0" )
print( 1 + 1 )
print( 1 + 1 .. 0 + 0 )
print( 1 + 1 .. 3 - 1 .. 2 * 1 .. 4 / 2 .. 5 % 3 )

This should give us the following when run with either:
Windows:

lua51.exe hello_all.lua

*NIX:

chmod +x hello_all.lua; ./hello_all.lua

We should get the following output:

./hello_all.lua 
Hello World!
10
2
20
22222

Variables

Lua is a dynamically typed language.
Lua defines a variable as [name] = [thing].
Variables are defined by their values rather than a type.
We can get the type via type( [variable] ).
We can use io.read() to get various values.

"*all"	reads the whole file
"*line"	reads the next line
"*number"	reads a number
num	reads a string with up to num characters

These can further be shortened down to:

*a
*l
*n
num

We can use io.write() to print without a new line unless defined via “\n” or print() to print content with a new line.
We can use if statements long term to sort all of this out.
We can combine all of this via:

io.write( "Who are you? " )
name = io.read()
io.write( "It's great to meet you " .. name .. "\n" )

io.write( "Give me a number: " )
a = io.read()
io.write( "Give me another number: " )
b = io.read()

io.write( "Type of a: " .. type( a ) .. "\n" )
io.write( "Type of b: " .. type( b ) .. "\n" )

io.write( "We have a+b=" .. a + b .."\n" )

We can get the following output:

Who are you? Some Dude
It's great to meet you Some Dude
Give me a number: 1
Give me another number: 1
Type of a: number
Type of b: number
We have a+b=2
Who are you? Some Dude
It's great to meet you Some Dude
Give me a number: 1
Give me another number: five
Type of a: number
Type of b: nil
/usr/bin/lua5.1: ./variables.lua:15: attempt to perform arithmetic on global 'b' (a nil value)
stack traceback:
	./variables.lua:15: in main chunk
	[C]: ?
Who are you? Some Dude
It's great to meet you Some Dude
Give me a number: 1 
Give me another number: 5five
Type of a: number
Type of b: number
We have a+b=6

Loops Part 1

Lua has several kinds of loops including: for, while, and repeat … until. These three types of loops are found in most programming languages, but the repeat … until is typically called do … while or something similar. Lua also has control logic with the standard if … then … elseif … else style control.

Operators

OperatorExampleMeaning
==A == BCompare if the value of A is equal to the value of B
~=A ~= BCompare if the value of A is NOT equal to the value of B
>A > BCompare if A is larger than B
>=A >= BCompare if A is equal to or larger than B
<A < BCompare if A is less than B
<=A <= BCompare if A is less than or equal to B

for Loops

for loops are basically the most fundamental kind of loop in general. There are several variants we will cover later, but the basic usage of the standard for loop is as follows:

for [initial value], [limit], [increment value] do
	[stuff]
end

An actual example of this would be:

for i = 0, 10, 1 do
	print( "i = " .. i )
end

while Loops

while loops look like follows:

while ( [condition] ) do
	[stuff]
end

For example:

answer = "5"
useranswer = ""

while ( useranswer ~= answer ) do
	io.write( "What is 3 + 2? " )
	useranswer = io.read()
end

repeat … until Loops

repeat … until loops are basically the same as while loops, except they will always run at least once through. The condition also goes at the end of the loop instead of at the top. They look like follows:

repeat
	[stuff]	
until ( [condition] )

To clean up the example while loop:

answer = "5"

repeat
	io.write( "What is 3 + 2? " )
	useranswer = io.read()
until ( useranswer == answer )

Basic if Statements

Here’s how this works out roughly:

if ( [condition A] ) then
	[stuff]
elseif ( [condition B] ) then
	[stuff]
elseif ( [condition C] ) then
	[stuff]
…
else
	[stuff]
end

An example looks like below:

io.write( "What's your name? " )
name = io.read()

if ( name == "Scott" ) then
	print( "Hi Scott!" )
elseif ( name == "John" ) then
	print( "What's up John?" )
else
	print( "Hey there, " .. name )
end

We can nest a conditional if into our loop. We can combine math with a comparison in an if statement. We also see that there is a precedence for operations as well. The standard mathematical order of operations is basically followed, and then we go into comparisons which have a similar order lower in the hierarchy. We can also theoretically use parentheses to force a different order. Another thing to note is that a lot of the parentheses used above are not, strictly speaking, necessary. For example, we could write code as follows:

j = 0
while j <= 20 do
	io.write( j )
	if j % 5 == 0 then
		io.write( " is divisible by 5!" )
	end
	io.write( "\n" )
	j = j + 1
end

Tables

Tables are an integral data structure in Lua. They operate like both standard arrays and like hash tables in other languages. The most basic use of a table is as an array. In our variables tutorial, we learned about basic variables, specifically scalars. We didn’t really dive into terminology then, but a scalar is basically a variable that holds one thing while an array holds multiple things in series.

The first thing to note about Lua arrays is that they are not a fixed size. A lot of languages require you to instantiate an array with a fixed size. This means that the array cannot be expanded past this size in a traditional sense (there usually exists some mechanism to expand arrays, but it can be extremely inefficient). The next important thing to note is that Lua arrays customarily start with an index of 1 rather than 0.

Arrays

Setting up a table as an array is very simple. Let’s start with:

animals = { "cat", "dog", "bird", "rat", "fish" }

This sets up an array of “animals” which has 5 elements. This is cool, but how do we access any of these elements? This is really easy, we just use:

array[ [index] ]

This will give us the value at the specified index. If we request a value which is not indexed, Lua will return nil, which is a special value that symbolizes a complete lack of value and is distinct from 0 or “”. Let’s try this out!

animals = { "cat", "dog", "bird", "rat", "fish" }

print( animals[ 1 ] )
print( animals[ 2 ] )
print( animals[ 3 ] )
print( animals[ 4 ] )
print( animals[ 5 ] )

print( animals[ 6 ] )

This results in:

cat
dog
bird
rat
fish
nil

Basic Table and Array Operators

table.insert( [table], [value] ) - Insert the value value at the end of table and resize accordingly
table.insert( [table], [position], [value] ) - Insert the value value at the position in the table and resize and reorder as necessary
table.remove( [table] ) - Remove the last element and resize the table
table.remove( [table], [position] ) - Remove the element at position and resize the table

To see this in action, we can run the following:

array = {}

for i=1, 5, 1 do
        io.write( "What do you want to put in the array? " )
        name = io.read()
        table.insert( array, name )
end

for j=1, 5, 1 do
        print( array[ j ] )
end

Which will look something like:

What do you want to put in the array? A
What do you want to put in the array? B
What do you want to put in the array? C
What do you want to put in the array? D
What do you want to put in the array? E
A
B
C
D
E

There are other ways to get the size of an array itself which can be fed into the original for loop such as the # operator and table.getn( [table] ) (though both of these do have some specific caveats). We can further clean this up by learning a new function, ipairs. ipairs is a simple function which takes a table and returns an interator and the value at said position in the array.

array = {}

for i=1, 5, 1 do
        io.write( "What do you want to put in the array? " )
        name = io.read()
        table.insert( array, name )
end

for j, value in ipairs( array ) do
        print( value )
end

print( "size: " .. #array )
print( "size: " .. table.getn( array ) )

Hash Tables

Hash tables or hash maps are the basis of many useful data structures. Ultimately, they end up being basically arrays on the back end with a way to map and retrieve values based on keys.

A visual example of how a hash function works to create a hash table.

This example shows a few keys being passed into a hash function and mapping into values. The values are shuffled around because they will typically be located in an array behind the scenes and the hash function exists as a way for the system to find these variables again.

Hash tables or, for the rest of this article, tables are instantiated in a similar fashion to arrays:

exampletable = {}

You can instantiate this type of table similar to an array, but note there are some key differences:

 exampletable = { ["Chicken"] = "Egg", ["Dog"] = "Puppy", ["Rabbit"] = "Kit" }

Accessing a table is very similar to accessing an array. You work with tables like follows:

table[ "key" ] = value

In this example table is the table name and key is the desired key. This will set the value as desired. So, you can write something like:

 exampletable = { ["Chicken"] = "Egg", ["Dog"] = "Puppy", ["Rabbit"] = "Kit" }

print( exampletable[ "Chicken" ] )
print( exampletable[ "Dog" ] )
print( exampletable[ "Rabbit" ] )

Luckily, we have something similar to ipairs, pairs. Pairs works basically just like ipairs except instead of an iterator and a value, it returns a key and a value. Do not ever expect the results in a table aside from an array to ever be returned in a sane or consistent order.

exampletable = {}

exampletable["Chicken"] = "Egg"
exampletable["Dog"] = "Puppy"
exampletable["Rabbit"] = "Kit"

for key, value in pairs( exampletable ) do
	print( "exampletable[ " .. key .. " ] = " .. value )
end

By setting a key to nil, we basically clear it out from existence.

Table Sorts

There is a sort function for tables (mainly arrays, but we will get to that in short order). It is extremely useful for both arrays and hash tables. The sort function is based on the quicksort algorithm.

table.sort( [table] )

Or:

table.sort( [table], [sort function] )

If you want to sort by a given sort function, you basically need a function which takes a and b and returns a boolean which is either true if a>b or false if b<a. The default sort function looks something like:

exampletable = {}

exampletable["Dog"] = "Puppy"
exampletable["Rabbit"] = "Kit"
exampletable["Chicken"] = "Egg"

array = { "Dog", "Chicken", "Rabbit" }
table.sort( array, function (a, b) return a < b end ) --this is the default

for i, key in ipairs( array ) do
	print( "exampletable[ " .. key .. " ] = " .. exampletable[ key ] )
end

As you can see, you can pass a function inline. Functions are first-class in Lua. You can pass them and treat them as variables for all intents and purposes. So, you could just have easily have passed the function as follows:

exampletable = {}

exampletable["Dog"] = "Puppy"
exampletable["Rabbit"] = "Kit"
exampletable["Chicken"] = "Egg"

array = { "Dog", "Chicken", "Rabbit" }
f =  function (a, b) return a < b end
table.sort( array, f ) --this is the default

for i, key in ipairs( array ) do
	print( "exampletable[ " .. key .. " ] = " .. exampletable[ key ] )
end

Multidimensional Tables and Arrays

The next feature we can take advantage of is the fact that all variables are pretty much equal in terms of functionality, so we can actually assign tables into tables, though how we do it is a little weird for certain cases. This is extremely useful for propping up certain data structures in a way which takes some heavy lifting in other languages. A quick example of this is as follows:

exampletable = {}

exampletable[ "Dog" ] = { ["head"] = 1, ["body"] = 2, ["skin"] = 3 }
exampletable[ "Rabbit" ] = { ["head"] = 2, ["body"] = 3, ["skin"] = 1 }
exampletable[ "Chicken" ] = { ["head"] = 3, ["body"] = 1, ["skin"] = 2 }

We can’t just print our values like we could previously. We need to get a little more creative and do something like follows:

exampletable = {}

exampletable[ "Dog" ] = { ["head"] = 1, ["body"] = 2, ["skin"] = 3 }
exampletable[ "Rabbit" ] = { ["head"] = 2, ["body"] = 3, ["skin"] = 1 }
exampletable[ "Chicken" ] = { ["head"] = 3, ["body"] = 1, ["skin"] = 2 }

for animal, features in pairs( exampletable ) do
	print( animal .. ":" )
	for key, value in pairs( features ) do
		print( key .. " = " .. value )
	end
end

Basically, when you work with structures like this, you need to nest for loops:

exampletable = {}

exampletable[ "Dog" ] = { ["head"] = 1, ["body"] = 2, ["skin"] = 3 }
exampletable[ "Rabbit" ] = { ["head"] = 2, ["body"] = 3, ["skin"] = 1 }
exampletable[ "Chicken" ] = { ["head"] = 3, ["body"] = 1, ["skin"] = 2 }

array = {}

for animal, features in pairs( exampletable ) do
	table.insert( array, animal )
end

f = function (a, b) return exampletable[ a ][ "head" ] < exampletable[ b ][ "head" ] end
table.sort( array, f )

for i, animal in ipairs( array ) do
	print( animal .. ":" )
	for key, value in pairs( exampletable[ animal ] ) do
		print( key .. " = " .. value )
	end
end

Loops Part 2

Boolean Statements

Lua includes two basic operators to help you stack Boolean comparisons as well as parentheses and similar operations which follow PEMDAS. The two operators are and and or. There are other operations which can be built out of these two, but these are not extremely important at this level. These two operations basically come after all of the PEMDAS operations with and coming taking precedence over or (see Programming in Lua’s Precedence section for more information).

and Statement

The first operation, A and B, basically translates to both A and B need to be true for the statement to be true. In this example, A and B can be anything which returns a Boolean value or similar in Lua. See below for some logical examples:

StatementABResult
true and falsetruefalsefalse
true and truetruetruetrue
false and falsefalsefalsefalse
1 and 2truetruetrue
1 and niltruefalsefalse
2 > 1 and not falsetruetruetrue

or Statement

The second item, A or B, basically translates to if A or B are true, the entire statement is true, and if A and B are both false, the entire statement is false. In Lua, A or B, does not boil down to the same basic statement of A or B in English. “A or B” in English basically boils down to either A or B, but most programming languages treat that as an xor which means an exclusive or. The following table covers some examples of Lua’s or statement:

StatementABResult
true or falsetruefalsetrue
true or truetruetruetrue
false or falsefalsefalsefalse
1 or 2truetruetrue
1 and niltruefalsetrue
2 > 1 and falsetruefalsetrue

This can further be reduced down to the following table as a comparison:

and Statementand Resultsor Statementor Results
true and truetruetrue or truetrue
true and falsefalsetrue or falsetrue
false and truefalsefalse or truetrue
false and falsefalsefalse or falsefalse

not Statement

in addition to and and or, we also have not which basically turns true to false and false to true. These types of inversions are extremely useful. See the chart below for a set of not constructions:

and Statementand Resultsor Statementor Results
not( true and true )falsenot( true or true )false
not( true and false )truenot( true or false )false
not( false and false )truenot( false or false )true

Examples

These various constructions can be used to remove some of the need to nest if statements and similar. We can change:

a = true
b = true
c = true

if a == true then
	if b == true then
		if c == true then
			print( "a, b, and c are true" )
		end
	end
end

Into:

a = true
b = true
c = true

if a == true and b == true and c == true then
	print( "a, b, and c are true" )
end

This could be further shortened into:

a = true
b = true
c = true

if a and b and c then
	print( "a, b, and c are true" )
end

Boolean Logic in Loops

These shortenings can carry over to while and repeat…until loops as well. This does not appear to be immediately useful, but can be extremely useful. We can see an example below where we fill in some example functions:

a = status()
b = other_status()

while a and b do
	operation()

	a = status()
	b = other_status()
end

While this example won’t do anything if we try to run it without filling in each function, we can see the value. Basically, you can carry over whatever Boolean operations to whatever conditional statements you use in Lua. Functions will be covered in short order, but these functions would just run an operation with no parameters, then set a to the results of status and b to the results of other_status without any parameters for either. Only for loops are an exception to this kind of structure (specifically because nothing should change their control variables), but they have their own variants which make up for this shortcoming.

Short Circuit Evaluation

One thing which has not yet been introduced is short circuit evaluation. Lua is like many programming languages in that it only evaluates operands when necessary. This means that if we run something like:

a = false
b = true

if a and b then
	print( "a and be are both true!" )
end

Then, our b is never actually evaluated. This is useful in that sometimes you can omit certain error handling if a piece of data has to be true to evaluate the second piece of data. For instance, you might need to use some error handling for something like:

a = get_data()
b = nil

if a ~= nil then b = special_function( a ) end

if a ~= nil and b ~= nil then print( a .. b ) end

We can basically shorten this to:

a = get_data()
b = nil

if a ~= nil then b = special_function( a ) end

if a ~= nil then print( a .. b ) end

Because we will never be able to run b ~= nil since we suppose our b depends on a. In our example, special_function( a ) is a little different than our other functions. This basically takes special_function and passes a to it as a parameter in the function. This is not important right now except to express that b requires a in order to actually be populated with any data. The b ~= nil will never be evaluated unless a is not nil. If b did not depend on a, then we could run into the issue of trying to concatenate a nil value to a string which would cause Lua to crash. In fairness, it still may be worth writing it the first way just to reduce bugs down the line if the function can return a nil for b depending on the function, but for the logic, it is not necessary with what we laid out.

Functions and Basics of Recursion

Functions

Functions in Lua are defined basically the same way as any other code block structure in Lua by using:

function [name] ([variables])
	[stuff]
end

Functions are “first-class values” and can be passed and treated as standard scalars for all intents and purposes.

Recursion

Recursion is basically the use of a function or similar which breaks down the problem and solves it at a small enough level. You can think of it like an onion. As you strip the layers off, you are effectively left with an onion, albeit a smaller one, until you reach the point you split it open and there’s nothing inside.

For example, to solve for a factorial you could use the following function:

function factorial ( a )
	if a <= 1 then return a end
	
	return a * factorial( a - 1 )
end

Libraries and Including Other Code

Lua includes several ways to include other code so that you could put your functions in one file then include it into the main program to make the code cleaner, or include other libraries of functions which may aid with whatever you’re doing. dofile will search in the local path first (i.e. where the program is run from), but can also be used to search and include code from pretty much anywhere. Let’s pretend we have all of our code in the same directory and we have a file like follows:
functions.lua

function do_something ( a )
	return a + 1
end

function do_nothing ( a )
	return
end

function return_cat ( )
	return "cat"
end

And then we have all of our main code in:

main.lua

#!/usr/bin/lua5.1

dofile( 'functions.lua' )

print( do_something( 2 ) )
do_nothing( 1 )
print( return_cat() )

Image by Markus Spiske from Pixabay