Lua: The Little Language That Could

2023-05-28

Lua is probably my favourite “little language” - a language designed to have low cognitive load, and be easy to learn and use. It’s embedded in a lot of software, such as Redis, NGINX via OpenResty and Wireshark. It’s also used as a scripting language in games such as World of Warcraft and Roblox via Luau. This post is a brief love letter to the language, with some examples of why I like it so much.

Simplicity

Lua is not a complicated language, with relatively few features and not a lot of syntax to learn. There are eight types:

There is no need to worry about float, int, usize. No need to worry about discrete structures to differentiate between arrays, dictionaries or structs. Even classes are “just” tables with a metatable. This simplicity makes it easy to learn and use, while providing enough power to do most things you need to do.

For example, let’s implement a simple binary search in Lua:

function binary_search(array, value)
    local low = 1
    local high = #array -- # is the length operator

    while low <= high do
        -- library functions are accessed via 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 this should be relatively familiar to look at, even if you’ve never seen Lua before. The only thing that might be unfamiliar is the local keyword, which is used to declare variables. Variables are global by default, so local is used to declare a variable as local to the current scope.

Compilability

Lua is an excellent target to compile to, due to its simplicity and easy C interop. This makes it a great choice for domain-specific languages (DSLs), such as Terra, MoonScript and Fennel. As a quick example, here is that same binary search implemented in MoonScript and Fennel:

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

Really, the true strength of Lua is that you can embed it almost anywhere - Lua is implemented as a library for a host program, like Redis. Traditionally, this was a C program, but there are implementations of a Lua VM in many languages, such as JavaScript via Fengari or Go via GopherLua. However, it has perhaps achieved most success as the primary scripting language for Roblox.

Perhaps one of my favourite uses is through HAProxy, harkening back to the days of Apache + mod_php scripting. Let’s set up a HAProxy configuration to respond to requests on a particular 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>&nbsp;&nbsp;--%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 we can 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>&nbsp;&nbsp;--William B. Sprague</p>
            </body>
        </html>

Why might we want to do this? It’s pretty easy to imagine wanting some small application logic on top of a web server, but not wanting to write a full web application. It can even be used to extend functionality on top of existing applications, like adding a small health endpoint to a Redis server:

-- this is a custom fork of redis-lua to support TLS
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 is like try/catch, takes a func and func args
    -- returns true/false and the return value of the func
	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 }
$ curl 127.0.0.1:8080/redis_health
{"ok":false}

## start redis
$ curl 127.0.0.1:8080/redis_health
{"ok":true}

This can be expanded even beyond this with register_action and register_fetches (see the docs) to intercept request information and modify it, or add additional auth capabilities to software that doesn’t support it natively.

Community

The Lua community is not particularly large, but there is a lot of excellent development occuring, with many libraries available through the Lua package manager, LuaRocks. From libraries for fast JSON parsing and encoding to an extended standard library, there is a little something for everyone.

A special mention must go to Leaf Corcoran, who wrote Lapis - a fantastic little web framework for the OpenResty distribution, which powers the LuaRocks website.

Conclusion

Is there really a conclusion? Lua is great, you can pick it up in a weekend and start taking advantage of it to write auth layers in HAProxy, World of Warcraft addons, a game in Roblox, script your window manager, map some networks or just small libraries that make you happy.