aum Posted May 29, 2023 Share Posted May 29, 2023 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: nil boolean number string userdata (used to represent C data structures, or blocks of raw memory) function thread (used for coroutines) table (an associative array, and the sole data structure in Lua) 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> --%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> --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. © 2023 Source Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.