Contact Us

Lua: The Little Language That Could

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.

Lua programming language logo

Simplicity

Lua has relatively few features and relatively little syntax. For example, the language has only 8 types:

  • nil
  • boolean
  • number
  • string
  • userdata (for representing C data structures or blocks of heap memory)
  • function
  • thread (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>&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 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 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.