Why is Lua so Popular?

If you’ll take a look at charts showing the fastest growing programming languages in open source projects, you are in for a big surprise. In the second place, following Rust closely, you’ll find this tiny little language called Lua.

Lua is a powerful, efficient, lightweight, embeddable, dynamically typed scripting language that supports object-oriented and functional programming.

Don’t worry, I know this is a mouthful, but we’ll deconstruct this definition and get the gist of it all.

Basics

So, Lua is efficient and lightweight since it relies on just 22 reserved keywords and 8 basic types. Even more interesting however, is its approach to Data structures. There are no classes, structs, interfaces or traits. There are no Arrays, Lists or HashMaps. The only data-structuring mechanism available in Lua is the Table. This might sound weird at first, but you’ll see in a second that this basic structure is versatile and powerful enough to cover all your needs.

local person = {
  name = "Awesome",
  age = 30,
  isProgrammer = true,
}

print(person.name)
print(person["age"])

person.language = "Lua"

for key, value in pairs(person) do
  print(key .. ": " .. tostring(value))
end

Thanks to its lightweight nature and the low barrier of entry Lua can also be easily embeddable in other host clients ranging from popular games like Roblox to modern IDEs like NeoVIM.

Ok, let’s get to the fun part and look at some code. We’ll review the main 7 language features you need to be aware of in order to know your way around the language.

Basic program

We are defining a local user variable pointing to the reference of Table. Since we can use string field names as indexes, this will make the Table behave like a record (or a dictionary).

local user = {
  name = "",
  age = 20,
}

In Lua functions are first-class values, so we can easily “attach” methods to our user object. The language is pretty flexible, and the hello method can be defined in the table directly as well. Note that the dot dot operator is used to concatenate strings. You’ll see throughout this article that Lua is full of these small quirks which make the language really interesting.

function user.hello()
  print("My name is " .. user.name .. " and I am " .. user.age)
end

Next, we are using some of the standard library support (we’ll get back to this later as well) to read information from the terminal, convert values to the correct type and populate our user record.

io.write("Name: ")
user.name = io.read()

io.write("Age: ")
user.age = tonumber(io.read())

Finally, we’ll make sure that the length of the name is larger than 0 using another interesting operator, and we’ll call the hello method if the validation passes.

if #user.name > 0 and user.age then
  user.hello()
end

We can run this code in the terminal by calling the Lua command followed by the file name.

lua main.lua

This should give you an idea about the very basics of the language. Next, let’s look at values and types, and then we’ll work our way up to some of the more meaty topics.

Types & Values

There are 8 types in Lua: string, number, boolean, nil (representing no value), table, function, thread, and userdata.

local name = "John"
local height = 6.6
local enabled = true
local user = nil

-- table
local list = {"red", "blue", "purple"}

-- function
local log = function()
  for key, value in pairs(record) do
    print(key, value)
  end
end

-- thread
local coroutine = coroutine.create(action)

All these are pretty self explanatory except maybe for the last two. We’ll discuss them in detail in a second, but for now remember that, despite its apparent simplicity, in Lua you can handle workloads via “collaborative multithreading”, and you can store and work with C data via userdata types.

Lua is a dynamically typed language, which means that variables do not have types; only values do. There are no type definitions in the language and all values carry their own type.

You might have also noticed that we are using the local keyword quite a lot. This is because in Lua variables are global by default. As you might know from other languages, polluting the global scope is error prone, so you should always try to limit the scope of your variables. As an additional benefit, using local variables leads to better performance, since local variables are stored on the stack, while global variables are stored in a special global environment table.

Control Flow

Next, let’s briefly look at Lua’s control flow. Things are a bit more verbose here, but simple nevertheless.

You have access to your usual if-then-else statements, while loops and also to repeat until loops which will execute your code at least once.

local number = 10
if number > 5 then
  print("Grater than 5")
elseif number < 5 than
  print("Less than 5")
else
  print("Number is 5")
end

local i = 1
while i <= 5 do
  print("Iteration " .. i)
  i = i + 1
end

local j = 1
repeat
  print("Repeat itertion " .. j)
  j = j + 1
until j > 5

The for statement can be numeric or generic to allow you to iterate over table values

-- numeric
for i = 1, 5 do
  print("Iteration " .. i)
end

-- generic
local langs = {"lua", "js", "c"}
for i, lang in ipairs(langs) do
  print("Language " .. i .. " is " .. lang)
end

Quick side note, I know that Lua’s syntax might not be your cup of tea, especially if your background is in languages which focus on concise semantics. However, keep in mind that we are talking about a 30 year old language, and the trends were a bit different back then.

Data Structures

Next, let’s take a deeper look at the Lua Table. As I already mentioned, this is the only data structuring mechanism, and is among the most powerful features of the language.

We can initialize an empty table like this, and then simply add fields using one of these approaches. Then, we can access field values again either via the dot notation or using square brackets. Of course, when we initialize a table just with values, this will act as an Array, with indexing starting from 1, just as God intended.

local table = {}

table["name"] = "John"
table.age = 2

print(table.name)
print(table["age"])

local array = {"a", "b", "c"}
for index, value in pairs(array) do
  print(index, value)
end

When working with data structures, operations like inserting, removing or sorting your data are fairly common, and these are provided in Lua thanks to the standard table library.

local languages = {"lua", "js", "rust"}

-- Insert at the end
table.insert(langauges, "java")
-- Inserting at a specific position
table.insert(langauges, 2, "c")

-- Remove the last element
table.remove(langauges)
-- Remove at a specific position
table.remove(langauges, 2)

table.sort(languages)
print(table.concat(languages, ", "))

Remember that tables, just like functions or threads are objects. In other words, variables do not actually contain these values, only references to them. Assignment, parameter passing, and function returns are always done by reference, so always keep an eye out on how you are mutating your data. In comparison Lua’s primitive types are immutable, and are always passed around by value.

This is a good time to briefly discuss Lua’s automatic memory management. Thanks to its Garbage collector, Lua’s memory is handled automatically, so you don’t have to worry about allocating or deallocating memory for your objects.

Metatables are another cool feature aimed to increase the flexibility of Tables. The easiest way to understand them is through an example.

Imagine we have a simple user object that’s printed on the screen. An internal “tostring”method is called on the object, which will return the memory address where the object is stored in the heap.

local user = {
  first = "John"
  last = "Doe"
}
print(user)
-- table: 0x6000004f07c0

We can change this by creating a metaUser Table, override one of the many operations you can control via metatables, and then link the user object with this new behavior.

local user = {
  first = "John"
  last = "Doe"
}

local metaUser = {
  __tostring = function(user)
    return user.first .. " " .. user.last
  end
}

setmetatable(user, metaUser)
print(user)

This is a powerful feature with great potential, and it is worth exploring in more detail. Just to give you an idea about the extent of this flexibility, we can define a “__call” method in our metatable and now, all of a sudden, our user object can also be called like a function.

local user = {
  first = "John"
  last = "Doe"
}

local metaUser = {
  __tostring = function(user)
    return user.first .. " " .. user.last
  end,
  __call = function(user)
    print("Tables can be functions?!")
  end
}

setmetatable(user, metaUser)
print(user)

Standard Library

I mentioned multiple times that Lua comes packed with a standard library offering useful functions implemented directly in C. One interesting such library offers support for coroutines which allow for non-preemptive multitasking.

Unlike threads in many programming languages, Lua coroutines are not executed in parallel but rather allow multiple sequences of operations to be interleaved in a cooperative manner.

Looking at another basic example, coroutines can be created based on functions which handle workloads and then yield control, or, in other words give up CPU time to allow other tasks to run.

function task1()
  for i = 1, 5 do
    print("Task 1, step " .. i)
    coroutine.yield()
  end
end

function task2()
  for i = 1, 5 do
    print("Task 2, step " .. i)
    coroutine.yield()
  end
end

local co1 = coroutine.create(task1)
local co2 = coroutine.create(task2)

for i = 1, 5 do
  coroutine.resume(co1)
  coroutine.resume(co2)
end

Working with C

Switching gears to some lower level yet still interesting aspects, Lua is known for its speed and efficiency mainly thanks to its C implementation. This makes the language portable and easily embeddable in other C based applications thanks to Lua’s C API that allows you to call Lua functions from C and pass data between the two languages seamlessly.

We can see this in action by looking at the following example. In a .c file let’s make sure we have access to the Lua APIs, and then define a Point structure. The new_point function allows us to create a Point instance by validating the data received from the stack and making sure to register a new userdata value.

Finally, we’ll register the “new point” function with Lua, compile our C code, and we should be ready to go.

#include <lua.h>
#include <luaxlib.h>
#include <lualib.h>

typedef struct {
  int x;
  int y;
} Point;

static int new_point(lua_State *L) {
  double x = luaL_checknumber(L, 1);
  double y = luaL_checknumber(L, 2);

  Point *p = (Point *)lua_newuserdata(L, sizeof(Point));
  p->x = x
  p->y = y

  return 1
}

int luaopen_mylib(lua_State *L) {
  lua_register(L, "new_point", new_point);
  return 0;
}

In a new Lua file, we can now load our C module, and use the provided functions and data structures directly.

require("lib")
local p = newPoint(10, 20)

Ecosystem

Finally, let’s evaluate Lua’s ecosystem to understand if there is enough 3rd party support and libraries we can use to avoid reinventing the wheel.

Lua offers a straightforward module system to package up code and share it easily, especially thanks to the LuaRocks package manager.

There are various successful libraries out there ranging well established web frameworks like Lapis to web application servers like Open Resty.

The community is alive and involved as well, and you’ll see various experiments like porting the JSX templating language to Lua or MoonScript - a dynamic scripting language that compiles into Lua. Until next time, thank you for reading!