Lua is probably my favorite "small language" — low cognitive load, simple to learn and simple to use. It is built into a lot of software, such as Redis, NGINX via OpenResty and Wireshark (and many others). It is also used as a scripting language in games like World of Warcraft and Roblox via Luau (and many others). This post is a brief love letter to the language, with a few examples of why I like it so much.

Simplicity
Lua has relatively few features and relatively little syntax. For example, the language has only 8 types:
nilbooleannumberstringuserdata(for representing C data structures or blocks of heap memory)functionthread(for coroutines)table(an associative array and, incidentally, the only built-in data structure)
No need to worry about float, int, usize. No need to worry about the differences between arrays, dictionaries and structs. Even classes here are just tables with designated metatables (think of prototypes, as in JavaScript). This simplicity across the board makes Lua easy to pick up and use, while still providing enough power to handle most of the tasks you need.
Let's implement a simple binary search over an array in Lua:
-- single-line comments start with two dashes
function binary_search(array, value)
local low = 1
local high = #array -- # is the length operator
while low <= high do
-- library functions are reached through the global table
local mid = math.floor((low + high) / 2)
local mid_value = array[mid]
if mid_value < value then
low = mid + 1
elseif mid_value > value then
high = mid - 1
else
return mid
end
end
return nil
end
res = binary_search({2, 4, 6, 8, 9}, 6)
print(res)
All of this should be fairly familiar, even if you have never run into Lua before. The one thing that may feel unfamiliar is the local keyword, used for declaring variables (and not only variables). By default all of them are global, so local declares a variable that is local to the current scope.
Transpilation
Lua is an excellent transpilation target, thanks to its simplicity and how easily it interops with C. So Lua is a good choice for domain-specific languages (DSLs), such as Terra, MoonScript and Fennel.
As a short example, here is the same binary search written in MoonScript and Fennel respectively:
binary_search = (array, value) ->
low = 1
high = #array
while low <= high
mid = math.floor((low + high) / 2)
mid_value = array[mid]
if mid_value < value
low = mid + 1
else if mid_value > value
high = mid - 1
else
return mid
return nil
print binary_search {2, 4, 6, 8, 9}, 6
(fn binary-search [array value]
(var low 1)
(var high (length array))
(var ret nil)
(while (<= low high)
(local mid (math.floor (/ (+ low high) 2)))
(local mid-value (. array mid))
(if (< mid-value value) (set low (+ mid 1))
(> mid-value value) (set high (- mid 1))
(do
(set ret mid)
(set low high)))) ; no early returns in Fennel
ret)
(local res (binary-search [2 4 6 8 9] 6))
(print res)
Embeddability
But the real strength of the language is that you can embed it almost anywhere — Lua is implemented as a library inside a host program, such as Redis. Traditionally that host was a C program, but today there are many Lua virtual machine implementations in other languages, such as JavaScript (with Fengari) or Go (with GopherLua). Still, one of the most popular implementations is the scripting language Luau for the game Roblox.
Perhaps one of my favorite uses of Lua is HAProxy, taking us back to the days of Apache + mod_php scripting. Let's set up an HAProxy configuration that answers requests on a given path with a random fortune:
local function fortune(applet)
local responses = {
{
quote = "The only people who never fail are those who never try.",
author = "Ilka Chase"
},
{
quote = "The mind that is anxious about future events is miserable.",
author = "Seneca"
},
{
quote = "A leader is a dealer in hope.",
author = "Napoleon Bonaparte"
},
{
quote = "Do not wait to strike until the iron is hot; but make it hot by striking.",
author = "William B. Sprague"
},
{
quote = "You have power over your mind - not outside events. Realize this, and you will find strength.",
author = "Marcus Aurelius"
}
}
local response = responses[math.random(#responses)]
local resp = string.format([[
<html>
<body>
<p>%s<br> --%s</p>
</body>
</html>
]], response.quote, response.author)
applet:set_status(200)
applet:add_header("content-length", string.len(resp))
applet:add_header("content-type", "text/html")
applet:start_response()
applet:send(resp)
end
core.register_service("fortune", "http", fortune)
global
lua-load fortune.lua
defaults
retries 3
timeout http-request 10s
timeout queue 1m
timeout connect 10s
timeout client 1m
timeout server 1m
timeout http-keep-alive 10s
timeout check 10s
frontend fe_main
bind :8080
mode http
http-request use-service lua.fortune if { path /fortune }
And then run it:
$ haproxy -f haproxy.cfg
$ curl localhost:8080/fortune
<html>
<body>
<p>Do not wait to strike until the iron is hot; but make it hot by striking.<br> --William B. Sprague</p>
</body>
</html>
Why do this at all? It is easy to imagine a situation where you need a small piece of application logic on top of a web server, but you do not want to write a full web application for it. Or you want to extend the functionality of existing applications — say, add a small endpoint that health-checks a Redis server:
-- this is a third-party fork of redis-lua with TLS support:
local redis = require("redis-tls")
local settings = {
host = "127.0.0.1",
port = 6379,
database = 14,
password = nil,
}
local utils = {
create_client = function(params)
local client = redis.connect(params.host, params.port, 1, false)
if params.password then
client:auth(params.password)
end
return client
end,
}
local function redis_health(applet)
-- pcall works like try/catch: it takes a function and its arguments,
-- and returns true/false plus the result of running that function
local ok, client = pcall(utils.create_client, settings)
if not ok then
local string_resp = '{"ok":false}\n'
applet:set_status(500)
applet:add_header("content-length", string.len(string_resp))
applet:add_header("content-type", "application/json")
applet:start_response()
applet:send(string_resp)
return
end
local resp = client:ping()
local string_resp = string.format('{"ok":%s}\n', resp)
applet:set_status(200)
applet:add_header("content-length", string.len(string_resp))
applet:add_header("content-type", "application/json")
applet:start_response()
applet:send(string_resp)
end
core.register_service("redis_health", "http", redis_health)
global
lua-load redis.lua
frontend fe_main
bind :8080
mode http
http-request use-service lua.redis_health if { path /redis_health }
The Redis server is down:
$ curl 127.0.0.1:8080/redis_health
{"ok":false}
The Redis server is up and reachable:
$ curl 127.0.0.1:8080/redis_health
{"ok":true}
You can go further and use register_action and register_fetches (see the docs) to intercept request information, modify it, or add extra authentication on top of software that has no built-in auth system.
Community
It is not especially large, but a lot of good work happens in it, and many libraries are available through the simple package manager LuaRocks. From libraries for fast JSON parsing and encoding to add-ons for the standard library (or even programmable computers in Minecraft) — there is something for everyone.
Special mention goes to Leaf Corcoran, who wrote Lapis — an excellent small web framework for the OpenResty distribution that the LuaRocks site runs on.
Unlike some communities, Lua has no small-scale or large-scale scandals or drama (see Rust 👀: ^1, ^2, ^3)
Conclusion
Is there any takeaway? Lua is very good: you can pick it up over a weekend and start using it to write authorization layers in HAProxy, add-ons for World of Warcraft, games within Roblox, scripts for your window manager, network tooling, or just small libraries that make you a little happier.
And don't forget the game engines — Love2D, Defold and Raylib integration.
Personally, I would recommend taking a look at Fennel, mentioned earlier — it is a Lisp that transpiles to Lua: you get Lua's speed, simplicity and accessibility with the flexibility of Lisp syntax and macros.