Lua at a Glance — A Language a Day, Advent Calendar Day 10/24

About this ‘A Language a Day’ Advent Calendar

Andrew Shitov. A Language a Day

This article became a part of my book ‘A Language a Day’, which you can get in both electronic and paper format. Languages covered in the book: C++, Clojure, Crystal, D, Dart, Elixir, Factor, Go, Hack, Hy, Io, Julia, Kotlin, Lua, Mercury, Nim, OCaml, Raku, Rust, Scala, and TypeScript.

Learn more where to get the book

Welcome to Day 10 of this year’s A Language a Day Advent Calendar. Today’s topic is introduction to the Lua programming language.

Facts about the language

Some facts about Lua:

  • A dynamically typed language
  • Strong focus on being implementable in ANSI C (which ensures it works on any platform, including embedded)
  • Appeared in 1993
  • Website: www.lua.org

Installing and running Lua

On a Mac, you can install Lua with a single command:

$ brew install lua

For other platforms, please refer to the official instructions.

Having Lua installed, you can run the program by passing the file name to the command-line tool:

$ lua hello-world.lua

Hello, World!

The Hello, World! program is short one-liner in Lua:

print("Hello, World!")

Comments in Lua

In this series, comments are almost never mentioned, but in Lua they deserve a word. A one-line comment starts with a double dash: --. A multi-line comment is enclosed between --[[ and --]]. What is sweet is that you can disable the comment by typing another dash in front of it:

--[[
print("Hello")
--]]

---[[
print("World")
--]]

The second print instruction is executed here. This is quite a nice feature for debugging, as it does not require you to remove the two pieces of the comment delimiters, and you also cannot make a mistake by commenting it back in a wrong place.

Variables

Variables are easy. You assign a value to an identifier and you are done.

name = "John"
age = 30

print(name .. " is " .. age)

What is less easy is that in Lua there are no traditional aggregate types such as arrays or dictionaries or sets or records. Instead, it offers tables.

Tables

A quick intro to the tables is here:

data = {}

data["alpha"] = 10
data["beta"] = 20
data[30] = "gamma"

print(data["beta"]) -- 20
print(data.beta) -- 20
print(data[30]) -- gamma

To create a table, use its constructor, {}. Then, you can utilise your knowledge of working with JavaScript arrays. Both strings and numbers are allowed as indices (actually, keys). When accessing the value stored under a string key, two notations are allowed: data["beta"] and data.beta.

Lua’s tables can serve as arrays or lists (notice that it is indexed from 1 in this case):

letters = {"alpha", "beta", "gamma"}
print(letters[2]) -- beta

They can work as dictionaries:

months = {Jan = "January", Feb = "February", Mar = "March"}
print(months.Mar) -- March

To remove a variable or an element of a table, assign the nil value to it.

Functions

Functions are defined with the function keyword. Functions can have parameters and they can return a value. Please take a look at the following snippet and notice that you have to declare variables with the local keyword. Otherwise, a global variable will be created, and you can use it outside of the function (or you will break an existing global variable).

function greet(name)
    local salutation = "Dear"
    print(salutation .. ' ' .. name .. '!')
end

greet("John")
greet("Alla")

-- print(salutation) -- visible if not "local" in "greet"

A Factorial example

Let us implement the factorial function using a loop.

function factorial(n)
    local result = 1
    for i = 2, n do
        result = result * i
    end
    return result
end

print(factorial(5)) -- 120

Notice that the result variable is declared local to prevent its visibility outside of the function, while the for loop counter does not needs to be protected: it will not be visible outside the loop itself.

Multiple results and variadic parameters

Functions can take variadic parameters and return multiple values (not necessarily together). In the following examples, both features are used to calculate the sums for a further statistical analysis of a data list:

function stats(...)
    local s = 0
    local s2 = 0
    for i, x in ipairs{...} do
        s = s + x
        s2 = s2 + x*x
    end
    return s, s2
end

sum, sum_sq = stats(10, 20, 30, 40, 42)
print("Sum = " .. sum)
print("Sum of squares = " .. sum_sq)

You can use the ... feature to pass all arguments to another function, e. g., for debugging:

function greet(salutation, name)
    print("Dear " .. salutation .. " " .. name .. "!")
end

function debug_me(...)
    print("Debug message")
    greet(...)
end

debug_me("Mr.", "Smith")

Here, you don’t care about the real number of arguments of the greet function. The definition of the debug function takes them all and brings them to another function.

Lambdas

You can save a function in a variable (they are so-called first-class values), and basically, you swap the name of the function with the function keyword to get a lambda. You can see this in the following example:

-- function sqr(x)
--     return x * x
-- end

sqr = function(x) return x * x end

print(sqr(4))
print(sqr(5))

There is another example of an anonymous function later when we’ll talk about coroutines.

Object-oriented facilities

There are no classes in Lua. Everything is organised via tables. As you have seen earlier, tables stand in place of lists, arrays, hashes, dictionaries, etc. They are also used for organising the object-oriented features. To understand how it works, we have to first look at the so-called metatables.

Metatables

A metatable is a table that carries some additional information about your other table-based object. You can associate your object with the given metatable. You can also share the same metatable with more than one object.

In the next example, there are two one-element lists, and I want to use the + operator to get a concatenated string composed of those values.

value1 = {4}
value2 = {2}
value3 = value1 + value2 -- 42

This code does not work, as there is no definition for the + operator for tables. We have to define this behaviour ourselves. And this is where metatables come in.

value1 = {4}
value2 = {2}

mt = {}
function mt.__add(a, b)
    return a[1] .. b[1]
end

setmetatable(value1, mt)
-- setmetatable(value2, mt)

value3 = value1 + value2
print(value3) -- 42

The mt variable is a table, and we add the __add method to it. This function takes two tables and concatenates their first elements.

To work with our variables, we have to associate the mt metatable with both value1 and value2. If you only use these objects so that it is always ‘added’ to another object of that kind, then it is enough to append the metatable to one of them only. In this case, both value1 + value2 and value2 + value1 work correctly.

Classes

Let us move on to emulating the prototype-based classes. Again, everything is built on top of tables.

I hope you already got an idea of a possible way to emulate classes. What you need to do is to define a number of methods in a table, and set it as a metatable of your object. The only problem is that you have to do it manually for every new object of that type that you create. There is a better way: define a kind of a constructor that will do that job for you.

Let us create the class for keeping the name and the age of a person. First, create the table:

Person = {}

Now, add a function to it:

function Person:new(o)
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    return o
end

Notice the semicolon in the function header. This is a syntax-sugar form of the following construct:

function Person.new(self, 0)

If you use Person:new, the compiler adds the self variable as the first parameter of this function.

The Person:new constructor does everything we need to initialise the metatable of the object. If no object is passed, then the o parameter gets the class itself (a class is an empty table, whose name starts with a capital letter).

Having that said, you can create new objects using a very straightforward code:

j = Person:new{name = "John", age = 24}
a = Person:new{name = "Alla", age = 22}

The line for creating j is equivalent to:

j = Person:new({name = "John", age = 24})

or:

j = Person.new(Person, {name = "John", age = 24})

Similarly, you can define a method:

function Person:info()
    return self.name .. " is " .. self.age .. " y.o."
end

The declaration of the function is equivalent to the following code with an explicit self:

function Person.info(self)
. . .

Now you can call this method on the object:

print(j:info()) -- John is 24 y.o.
print(a:info()) -- Alla is 22 y.o.

Let me once mention it once again that j:info() is the same as Person.info(j).

A Polymorphic example

Inheritance can be expressed via creating a table of the derived class using the constructor of the base class. You can see it in the following example.

Animal = {}

function Animal:new(o)
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    return o
end

function Animal:legs()
    return 4
end

Dog = Animal:new()
function Dog:info()
    return "Dog"
end

Cat = Animal:new()
function Cat:info()
    return "Cat"
end

zoo = {Dog:new(), Cat:new(), Cat:new()}
for i, x in ipairs(zoo) do
    print(x.info() .. " has " .. x.legs() .. " legs.")
end

Here, both the Dog and the Cat classes start with being an Animal object.

To demonstrate a common method, we defined the Animal:legs() function, which is accessible for both cats and dogs.

In the loop over different objects, both info() and legs() are called. The legs() method is dispatched to Animal:legs(), while the destination of info() depends on the type of the object.

Coroutines

Asynchronous execution in Lua can be implementing using coroutines. They are not threads, and are executed in such a way that only one coroutine is running at any given moment.

For working with coroutines use the methods of the coroutines table.

cr = coroutine.create(
    function() print("Hello, World!") end
)

coroutine.resume(cr)

To start a coroutine, you need to call coroutine.resume.

Sleep Sort

Here is a slightly modified example from Rosettacode. Unlike the examples in previous articles in this series, the code is quite long.

function sort_me(n)
    local t0 = os.time()
    while os.time() - t0 <= n do
        coroutine.yield(false)
    end

    print(n)

    return true
end

data = {10, 4, 2, 6, 2, 7, 1, 3}
jobs = {}

for i = 1, #data do
    job = coroutine.wrap(sort_me)
    table.insert(jobs, job)
    job(data[i])
end
  
done = false
while not done do
    done = true
    for i = #jobs, 1, -1 do
        if jobs[i]() then
            table.remove(jobs, i)
        else
            done = false
        end
    end
end

There a quite a few interesting moments in this code.

First, the delay operation is implemented with a loop where you check the time passed and unblock a coroutine by yielding (in the same sense as in Python) the false result if the required duration is not reached yet.

Second, the jobs (which are coroutines) are wrapped and saved in the table. Notice how you expand the array: table.insert(jobs, job), and how later you do the opposite by removing an element at a given index: table.remove(jobs, i). The argument is passed to the wrapping object.

Also notice how you can loop backwards with a negative step: for I = #jobs, 1, -1.

Get more

To learn more about Lua, I would recommend you to use the documentation from the official website and to read the book by the author of the language, Programming in Lua (2nd edition).

The source codes for today’s article are located in the Lua directory of this project on GitHub.

Next: Day 11. Raku

Leave a Reply

Your email address will not be published. Required fields are marked *

Retype the CAPTCHA code from the image
Change the CAPTCHA codeSpeak the CAPTCHA code