little tongue that could

Lua is perhaps my favorite “small language”, low cognitive load and easy to learn and use. It is built into many software such as Redis, NGINX via OpenResty And Wireshark (approx. translation.: and many others). It is also used as a scripting language in games such as World of Warcraft And Roblox via Luau (approx. translation.: and many others). This post is a short declaration of love for the language with some examples of why I like it so much.

Lua programming language logo

Lua programming language logo

Simplicity

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

  • nil

  • boolean

  • number

  • string

  • userdata (to represent C data structures or blocks of memory on the heap)

  • function

  • thread (for coroutines)

  • table (an associative array and, concurrently, the only built-in data structure)

No need to worry about float, int, usize. No need to worry about differences in arrays, dictionaries and structures. Even the classes here are just tables (table) with the specified meta tables (metatables, approx. transl.: think of prototypes like in JavaScript). This simplicity throughout makes Lua easy to learn and use, while providing enough power to perform most of the necessary tasks.

Let’s implement a simple binary search on an array in Lua:

-- однострочные комментарии начинаются с двух тире

function binary_search(array, value)
    local low = 1
    local high = #array -- # это оператор взятия длины

    while low <= high do
        -- доступ к библиотечным функциям через глобальную таблицу
        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 relatively familiar, even if you’ve never encountered Lua before. The only thing that may seem unusual is the keyword localwhich is used to declare variables (approx. translation: and not only). They are all global by default, so local is used to declare a local variable relative to the current scope.

transpilation

Lua is an excellent target for transpilation due to its simplicity and ease of interoperability with C. Therefore, Lua is an excellent choice for domain-specific languages ​​(DSLs) such as Terra, moonscript And Fennel.

As a quick example, here is the same binary search written in MoonScript and Fennel respectively:

Code
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 true power of the language is that you can embed it just about anywhere – Lua is implemented as a library for a host program like Redis. Traditionally, this was a C program, but now there are many implementations of the Lua virtual machine in different languages, such as Javascript (with Fengari) or Go (with GopherLua). However, one of the most popular implementations is a scripting language. Luau for Roblox game.

Perhaps one of my favorite uses of Lua is HAProxy, which takes us back to the days of Apache + mod_php scripting. Let’s set up a HAProxy configuration that will respond to requests along a specific path with a random prediction:

Code
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’s easy to imagine a situation where you need a little bit of application logic on top of a web server, but you don’t want to write a complete web application for that task. Or you want to extend the functionality of existing applications, for example, add a small request (endpoint) to check the status (healthcheck) of the Redis server:

Code
-- это сторонний форк redis-lua с поддержкой 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 как try/catch, принимает функцию и аргументы,
	-- и возвращает true/false и результат выполнения функции
	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 }

Redis server is down/down:

$ curl 127.0.0.1:8080/redis_health
  {"ok":false}

Redis server is up and running:

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

You can go ahead and use register_action And register_fetches (cm. docks) to intercept information about the request, modify it, or add additional authentication capabilities on top of software without a built-in authentication system.

Community

It’s not very big, but it has a lot of great development and many libraries are available through a simple package manager. LuaRocks. From libraries to fast parsing and JSON encoding before additions to the standard library (approx. translation.: or even adding programmable computers in minecraft) – there is something for everyone.

Worthy of special mention Leaf Corcoranwho wrote Lapis is a fantastic little web framework for the OpenResty distribution that powers the LuaRocks website.

Note. translation.: Unlike some communities, there are no small or big scandals or intrigues in Lua (see Rust 👀: ^1, ^2, ^3)

Conclusion

Is there any conclusion? Lua is very good, you can master it over the 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, networking or just little libraries that make you a little bit happier.

Note. translation.: and don’t forget about game engines – Love2D, Defold And Raylib integration.


Personally, I would recommend looking at Fennelmentioned above in translation. It’s LISP transpiled to Lua – you get the speed, simplicity, and accessibility of Lua with the flexibility of LISP syntax and macros.

And for those who are just starting their way in programming, I have a promo code HABR23. I won’t say where to apply, otherwise it’s advertising 🙂

By the way, I run a Telegram channel for those interested in learning and understanding everything in IT, join if you haven’t already!

Similar Posts

Leave a Reply

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