DEV Community

Cover image for The Lua Tutorial
DR
DR

Posted on • Updated on

The Lua Tutorial

Introduction

My goal for this post is to run through the basics of the Lua scripting language. This is stuff I've learned over the past few days, and it's not the deepest explanation you'll ever see, but it's how I'm interpreting the language so I'll note it that way.

If you haven't set up an environment or don't really know what I'm talking about, I'd recommend reading my last post that went through our main goals and installation processes.

And no, I don't fully intend for you to read this one top to bottom. It's a lot of stuff, and probably pretty dry unless you like learning languages in an hour (which I've tried, it's not fun). It's for your reference - not for a thrilling read.

That being said, it's the springboard for interesting things that I'll be making in the coming month, so it's important. Additionally, if you've ever wanted to learn Lua (or any other language I'll be playing with over the year), now's the time! I'm putting all of this material out there in the hopes that someone will find it useful - even if not today, then maybe in the future.

Without further ado, let's get into Lua.

Basics

The print function outputs a line to the console. If multiple parameters are passed, it prints each one on the same line with a tab character (\t) as the delimiter. This function outputs any data type with a type coercion to string similar to JavaScript.

print("Hello, world!") -- "Hello, world!"
print(1, 2, 3)         -- "1    2    3"
Enter fullscreen mode Exit fullscreen mode

You might also notice the -- next to the intended output - that's a comment. Comments, as in any other language, aren't executed when it comes time to run the code. This makes them useful for explaining why the code works in human language without having the computer try to understand them.

Variables are relatively simple - there's no explicit type declaration, and there are only a few technical things to keep in mind when declaring and assigning them.

x = 5 -- global variable x is initialized with a value of 5

do
    local x = 10 -- local variable x is initialized with a value of 10
    print(x) -- 10
end

print(x) -- 5 (global value)
Enter fullscreen mode Exit fullscreen mode

A variable's scope is determined by its block (the do..end in the code simply creates another block, similar to {} in JavaScript). Local variables are limited to their chunk or block, while global variables are available throughout the namespace.

I'll quickly note that Lua allows multi-variable assignment in one line, unlike some other notable languages.

a, b = 5, 4 -- a is now equal to 5, b is equal to 4
Enter fullscreen mode Exit fullscreen mode

Now, we can talk data types - classifications for the data that variables hold. There are four basic data types in Lua - strings, numbers, Booleans, and nil.

Strings

  • A string is an immutable sequence of characters which can be declared with single quotes, double quotes, or double brackets.
  • When using single or double quotes, you can use escape characters to use quotes and other escape sequences (full list found here).
single = 'It\'s a string.'
double = "This is also a \"string\"."
bracket = [[
Here's a multiline string.
Escape characters don't work in here!
]]

print(single)
print(double)
print(bracket)

--[[

Output:

It's a string.
This is also a "string".
Here's a multiline string.
Escape characters don't work in here!

]]
Enter fullscreen mode Exit fullscreen mode

It may also be noted that --[[ comment ]] is a multiline comment. This might make more sense now that double bracket strings have been introduced, because in the grand scheme of things it's just commenting out a multiline string.

Numbers

  • A number stores a numeric value (positive, negative, and zero) in a double-precision floating-point form. Constants may include optional decimal and exponent parts.
num_1 = 3
num_2 = 1.23
num_3 = 4e+2 -- 4 * 10^2

print(num_1)
print(num_2)
print(num_3)

--[[

Output:

3
1.23
400.0

]]
Enter fullscreen mode Exit fullscreen mode

Booleans

  • A Boolean value represents true or false. When other values are tested in a condition, everything except false and nil evaluate to true.
bool_true = true
bool_false = false

print(bool_true)
print(bool_false)

--[[

Output:

true
false

]]
Enter fullscreen mode Exit fullscreen mode

Nil

  • nil is the absence of a value. It's assigned to global variables by default, and you can assign it to a global variable to, in essence, delete it.
nil_value = nil

print(nil_value)

--[[

Output:

nil

]]
Enter fullscreen mode Exit fullscreen mode

As a final note, you can use type to get the data type of any variable.

str = "hi"
num = 4
bool = true
n = nil

print(type(str))
print(type(num))
print(type(bool))
print(type(n))

--[[

Output:

string
number
boolean
nil

]]
Enter fullscreen mode Exit fullscreen mode

Operators

Operators perform operations on data. Whether it's in variable form or a constant value, Lua provides several helpful operators spread among three main categories that lend needed functionality to the language.

Arithmetic

This is the kind that we're all familiar with: plus, minus, multiply, divide. Simple math concepts that make it possible to put that calculator logic into your programs.

We'll start this entire operator section with two variables: num_1 and num_2.

num_1, num_2 = 11, 2
Enter fullscreen mode Exit fullscreen mode

Note that we're using the multi-variable assignment pattern, mentioned earlier in our discussion of variables.

There are seven main arithmetic operators in Lua: +, -, *, /, ^ (exponentiation), % (modulus or remainder), and - (negation). Yes, the negation and subtraction operators are the same symbol - don't get them confused. One is used in a unary fashion on one variable (negation), and the other is used to combine two pieces of data (subtraction).

print(num_1 + num_2) -- 13
print(num_1 - num_2) -- 9
print(num_1 * num_2) -- 22
print(num_1 / num_2) -- 5.5
print(num_1 % num_2) -- 1
print(num_1 ^ num_2) -- 121.0 (converts to float)
print(-num_1) -- -11
Enter fullscreen mode Exit fullscreen mode

There's also an eighth, newly added operator that does floor division or integer division (//). However, it's only available in versions 5.3 and up, so in an effort to be inclusive to all versions I'll just be going about the old divide-and-round-down method.

Relational

Relational operators compare data instead of performing an operation on it as the arithmetic operators do. They're the ones that we'll use in things like conditionals and loops, because they return a Boolean value that evaluates to true or false.

There are six of these: == (equal), ~= (not equal), < (less than), > (greater than), <= (less than or equal to), and >= (greater than or equal to).

print(num_1 == num_2) -- false
print(num_1 ~= num_2) -- true
print(num_1 < num_2) -- false
print(num_1 > num_2) -- true
print(num_1 <= num_2) -- false
print(num_1 >= num_2) -- true
Enter fullscreen mode Exit fullscreen mode

Logical

Logical operators work with Boolean values to give programs a structured logic. Using and, or, and not, we can create more complex conditional checks and combine the operators we have already for more readable code.

-- The and keyword returns the first argument if it is false and the second argument otherwise.
print(num_1 and num_2) -- 2
-- The or keyword returns the first argument if it is true and the second argument otherwise.
print(num_1 or num_2) -- 11
-- The not keyword returns the opposite (true -> false, false -> true) of the given value.
-- Any value except nil and false itself is treated as a true value.
print(not num_1) -- false
Enter fullscreen mode Exit fullscreen mode

Miscellaneous

There are an additional two operators that I'll note: .. and #.

The concatenation (..) operator combines two strings. The # operator gets the length of a string or table (another data structure we'll get to in a second).

str_1, str_2 = "This is one half", "and this is the other half."

print(str_1 .. " " .. str_2) -- This is one half and this is the other half.
print(#str_1) -- 16
Enter fullscreen mode Exit fullscreen mode

Control Structures

The first basic type of control structure is a conditional. This is the classic if-else block that many programmers will recognize from other languages. The formatting is similar to many other scripts as well, with a few differences. Here's the main form.

if condition then
    -- condition == true
elseif other_condition then
    -- other_condition == true and condition == false
else
    -- other_condition == false and condition == false
end
Enter fullscreen mode Exit fullscreen mode

This example obviously includes all three keywords: if, elseif, and else. In reality, all you need is the if to create a conditional, but these extra are provided to give more depth. As with other languages, you can create as many elseif conditions as you need. Just remember to cap it with an end to show that the block has completed!

Here's one more example for good measure.

num_1 = 5
num_2 = 2

if num_1 > num_2 then
    print(num_1 .. " is greater than " .. num_2)
elseif num_1 < num_2 then
    print(num_1 .. " is less than " .. num_2)
else
    print(num_1 .. " is equal to " .. num_2)
end
Enter fullscreen mode Exit fullscreen mode

Besides conditionals, Lua also offers three types of loops: while, for, and repeat.

The while loop takes a condition and loops through a set of given statements until the condition is true. It is worth noting that the statements are initially only executed if the condition is true; if it is false then they won't be executed at all and the program will continue onward.

i = 0

while (i < 10)
do
    print(i)
    i = i + 1
end -- returns 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Enter fullscreen mode Exit fullscreen mode

Note the do...end statements, creating a block of code.

The for loop takes a series of conditions and executes a group of statements a known number of times (given by the conditions). It's generally used for when we know the number of times that we need to iterate through, whether that's a hard-coded constant or a variable.

for j = 0, 10, 1
do
    print(j)
end -- returns 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Enter fullscreen mode Exit fullscreen mode

The conditions work like this: in the first argument, we pass a variable that will be manipulated by the loop structure. The second argument is a cap that ends the loop when the first initial variable gets to it. The third is the step value, which can be negative as well as positive. If you're confused, here's the official reference page which clears some things up.

The repeat loop is a while loop that checks at the end instead of at the beginning. It has a completely different set of use cases, but it's still aimed at situations where we don't know the exact number of iterations.

k = 0;

repeat
    print(k)
    k = k + 1

    if (k == 7) then
        break;
    end
until (k > 10) -- returns 0, 1, 2, 3, 4, 5, 6
Enter fullscreen mode Exit fullscreen mode

You'll notice that I also made use of the break keyword. When used in any of these three loops, execution of the statements is immediately halted (as we can see with the example above).

Functions

Functions in Lua are relatively straightforward. A simple function in any language has a few basic components: a name for future use, a place to include parameters from outside the function, and a way to return values from the function to the outside sphere. Lua includes all three of these in a basic function format.

function square(number)
    return number * number
end

print(square(3)) -- 9
Enter fullscreen mode Exit fullscreen mode

Note the classic Lua form of creating a new chunk between the function and end keywords. This of course also means that number is undefined outside of the function, so don't try to access it.

As a side note, parameters that are not filled relapse to a value of nil - so keep track of your function calls to avoid any unexpected behavior!

Tables & Iterators

There's only one advanced data type in Lua - and it's not an array, object, dictionary, or tuple. It's called a table, and it's a flexible listing option that combines the best of several advanced data types in other languages.

The #1 thing to remember when dealing with tables is that they're indexed starting at 1. If you create a table like the one below, you might be surprised at what this means.

fruits = {"Banana", "Orange", "Apple"}
print(fruits[1]) -- Banana
Enter fullscreen mode Exit fullscreen mode

However, they also function similarly to JavaScript objects in that you can set values equal to certain keys without listing the values or keys in the original object. My favorite part of this? We can use all sorts of odd characters (even emojis!) to represent keys.

fruits = {"Banana", "Orange", "Apple"}
fruits["🥭"] = "Mango"

print(fruits[1]) -- Banana
print(fruits["🥭"]) -- Mango
Enter fullscreen mode Exit fullscreen mode

To remove an element, use the table.remove method. This method has one optional parameter (the 1 below), showing which element to remove. If this isn't specified, it removes the last item.

fruits = {"Banana", "Orange", "Apple"}

print(fruits[1]) -- Banana
print(#fruits) -- 3

table.remove(fruits, 1)

print(fruits[1]) -- Orange
print(#fruits) -- 2
Enter fullscreen mode Exit fullscreen mode

To insert an element, use the table.insert method. This method has one optional parameter. Usually, it'll take two: the table to alter and the value to add, but it can also take a third which is an index.

fruits = {"Banana", "Orange", "Apple"}

print(fruits[1]) -- Banana
print(#fruits) -- 3

table.insert(fruits, "Mango") -- insert Mango to the fruits table at the last index
table.insert(fruits, 1, "Papaya") -- insert Papaya to the fruits table at index 1

print(fruits[1]) -- Papaya
print(#fruits) -- 5
Enter fullscreen mode Exit fullscreen mode

To put the entire table into a string, use table.concat. This method combines all of the values into a single string using an optional delimiter.

fruits = {"Banana", "Orange", "Apple"}

print(table.concat(fruits)) -- BananaOrangeApple
print(table.concat(fruits, " and ")) -- Banana and Orange and Apple
Enter fullscreen mode Exit fullscreen mode

To sort a table, use table.sort.

fruits = {"Banana", "Orange", "Apple"}

print(fruits[1]) -- Banana
table.sort(fruits)
print(fruits[1]) -- Apple
Enter fullscreen mode Exit fullscreen mode

Iterators

Often when we're working with tables, we want to iterate through them in order to perform some sort of action or maybe just print the whole thing accurately. For this purpose, I present the iterator.

fruits = {"Banana", "Orange", "Apple"}

for k, v in ipairs(fruits) do
    print(k, v)
end

--[[

1       Banana
2       Orange
3       Apple

]]
Enter fullscreen mode Exit fullscreen mode

Using ipairs, we can quickly loop over tables. More importantly, we have the exact values of each key and value - which will come in more use when we start building bigger programs.

Alternatively, you can go the primitive route - which gets the same result. I'd encourage going with the industry standard and using ipairs, though.

fruits = {"Banana", "Orange", "Apple"}

for i = 1, #fruits, 1 do
    print(i, fruits[i])
end

--[[

1       Banana
2       Orange
3       Apple

]]
Enter fullscreen mode Exit fullscreen mode

File I/O

When dealing with reading and writing to files in Lua, we have two choices: either implicit or explicit file descriptors. They both use the same io library, but are accessed in different ways.

Let's look at implicit first.

file = io.open("test.txt", "a")
io.output(file)

io.write("Writing to the test file!")

io.close(file)
Enter fullscreen mode Exit fullscreen mode

Firstly, Lua will create a file if one isn't already existing. Secondly, we open the file in append mode ("a"), and set the output to that file. In explicit descriptors, we'll skip this step. Finally, we'll write a line to the file and close it to prevent leaks.

Explicit file descriptors are only a bit different. Here's the exact same example with them implemented instead of the implicit style.

file = io.open("test.txt", "a")

file:write("Writing to the test file!")

file:close()
Enter fullscreen mode Exit fullscreen mode

As you can see, it's much shorter because we're directly calling the methods on the file. In addition, it's easier to manage when you're reading and writing from and to multiple files at once, because you don't have to keep calling io.output with each different name.

I/O in Lua is a fascinating concept, which I'm hoping to explore more deeply in the future. If you're curious, here's the official documentation for the io library as well as more example for usage.

Conclusion

2,900 words later, I've reached the end. But this is really only scratching the surface. My respect for this language has definitely grown - it's fun to compare it to other languages and see what's the same and what's different.

So what's after the basics? We're going to start making programs (small ones to start with). For the next week, I'll be making programs to test my newfound knowledge - I'm posting them all with the code next Friday, don't miss it.

Until then... I'm going to go take a nap. Cheers!


Hey there! Thanks for reading my post. If you enjoyed, leave a like and share 💖

If you're new to the series, I'm breaking down twelve popular programming languages over the year of 2024 and exploring what they have to offer as opposed to others. This is, of course, a valiant attempt to revive my programming "career" and publish interesting content.

Want to keep up with my shenanigans? Check out the GitHub repository, which is where I'm stockpiling my projects over the year. You can also follow me here on DEV to get my latest posts straight to your notifications!


Top comments (0)