Lua - almost perfect

To continue my 4-month hiatus from my hatred-fueled Python rant, I will be taking a look at a language that doesn't engulf me in the flame of hatred every time I head its name uttered - Lua. Lua is a delightful scripting language, devised by the great minds of PUC in the mid-90s. The main goal of the language is portability, and its final executable is a whopping, mind-numbing... 300KB (you can literally fit this shit on an Arduino). Also, unlike some other filthy snake-like languages, Lua can run on pretty much whatever you throw at it.

If you haven't given Lua any thought or time, I strongly encourage you to stop right here, download the single .exe that is Lua and fuck around with it for a few hours. Yes, the 1-indexed arrays are weird, but they grew on me (more on that later). Also, the begin-end-esc syntax might put you off, but it actually is consistent and makes sense (unlike no semicolons on the end of the line, yes, me and my bloodline will die on this hill).

PS: I'm sorry that this isn't very well-structured, but life is such sometimes, I absolutely suck at writing :/

The good

Before we get to the bad, we have to go trough the good. And BOY is there a lot of good to talk about in Lua.

For starters, it isn't the brainchild of an insane old french guy (aka Rossum). Instead, it has been fully designed by the good people at the PUC Rio university, and it was designed with a few goals in mind (I will not back up these points with any source because I don't want to):

To be honest, these are solid goals. Of course, the key is in the execution, and certain languages are shining examples of setting good goals but fucking it up big time. Lua, however, is an example of how you design a good language. It never forces its opinions on you - its authors certainly had some preferences, but if you don't like them, you can do whatever the fuck you want. Don't like OOP? You don't have to do it in Lua. You can go purely procedural or functional. You like your code with semicolons? Lua won't scream at you if you place a semicolon at the end of the line. Don't like the built-in libraries? Fuck me if PUC cared, go ahead and make your own, its actually relatively simple with the great C API.

Another aspect of Lua that I truly love is that it is a really well thought out language. You never get the feel that it was rushed or the authors did something because it "looked cool". Unlike Python, using this language actually feels like using a genuine tools developed for people that can look at the outer walls of an asylum.

Yes, at first, the begin-end syntax may put you off, but then you realize that 1. it isn't too bad and too unlike the braces and 2. it allows you to use both the member call syntax of obj:method(a, b, c), as well as the paren-less call syntax of func { a, b, c } in an if or a loop condition. Yes, curly braces are the defacto standard, and even I would've told you 3 months ago that anybody would be insane for thinking begin-end is a good idea, but it's just another way of expressing the same idea.

Another great thing about Lua are its objects, or should I say, tables. It is in the core of the language's semantics, and I can literally explain them in about three sentences. Ok, here we go:

Tables are basically hashmaps that can have keys and values of any types. Integer keys are considered by convention elements of an implicit array. Tables also have an attached "metatable", which describes, amongst other extensions of the table's functions, its __index, or in JS terms, its prototype.

Done! That's literally all tables are. And it's fucking enough. You can achieve the expressiveness level of any language you pick with this construct.

Lua also absolutely nails the OOP model - it resembles the ES5 days of JavaScript, if JS was not bloatware. First, you define your class table, then you populate it with your functions, and finally, you define a constructor (which can just be the new member of the table), which just returns a new table and sets its __index metafield to the class table. Another nice addition that Lua introduces to sweeten the OOP semantics are the member calls and member definitions, obj:method(...), equivalent to obj.method(obj, ...), and function obj:method(...) end, equivalent to function obj.method(self, ...) end, respectively.

Speaking of function definitions, Lua has devised a very intelligent syntax for function definitions. You basically have three syntaxes that produce functions:

-- Value expression
local test = function (a, b, c) ... end

-- Defines a local and assigns the function as its value
local function test(a, b, c) ... end

The more keen-eyed of you have noticed that I'm leaving one out. This is because I'm leaving the best for last - function assignments:

-- This will assign to the global test
function test() end

-- This will assign to "func"
local func;
function func() end

local table = {};
-- The best one yet; This will assign to table.func
function table.func() end
-- I mentioned this above, this will give us an implied first argument of "self" (nice)
function table:func() end

Lastly (there is lots more that can be said, but let's keep this brief), we have multi-values. For newcomers there really isn't a better way of seeing its elegance other than fucking around with it. Other than having multi-return functions (all languages need to have this), you can also use multi-expressions (expressions that produce multiple values) in the end of multi-value consuming expressions (the last argument of a call, the last element of an array, the last element of an assignment list, etc.), and it will just append the multi-return values to the end of the value list. I believe that an example is needed:

local function func()
	return 1, 2, 3;
end

local a, b, c, d, e;

a = func(); -- 1
a, b, c = func(); -- 1, 2, 3
a, b, c = 1, 2, func(); -- 1, 2, 1
a, b = func(), "kun4o"; -- 1, "kun4o"

local table = { func() }; -- { 1, 2, 3 }
print(func()); -- 1, 2, 3
-- and etc

Of course, we can't leave the other big part of multi-value semantics - variadic arguments. You can basically declare any Lua function as being variadic by adding a pseudo-parameter ... at the end of the function's parameter list. After that, in the function's body, ... will actually be an expression, which will evaluate to all the excess arguments that were passed to the function. This, right here, might be the most beautiful part of Lua. Like, how do you think of shit that good?

Coroutines

I have dedicated a whole section to this, because it is simply one of, if not the best features I've seen in a language. In essence, it allows you to create a pseudo-thread - it behaves like a thread, but instead of running in parallel, there is only one thread ever running. The crucial part is that you can transfer control between two threads, and the stack of each thread will be preserved. This means that, from any point of the call stack in a thread, you can suspend the execution of the current thread and, at a later time, be given the control back, from the same place.

Combined with an event loop, this basically solves the problem with async-await by making all functions async. Honestly, if the web had adopted Lua instead of inventing the brain fart that JS is, we would probably be drinking expensive booze in Mars, laughing about how unfortunate we were before Lua, and wouldn't have to deal with async/await, the callback hell, NodeJS's initial resistance to adopt said async/await, etc. etc.

The only issue is that Lua implements asymmetric coroutines (you resume a specific coroutine and it yields back to whichever coroutine resumed it). This presents one big issue: yields across callbacks. If you have a function that uses a coroutine and inside that coroutine, it calls a function that yields, but was supposed to yield to a different function, the control will end up at the wrong coroutine, causing major fucking headaches.

For this purpose, we can apply the band-aid, called "coro", which implements symmetric coroutines (you just transfer to another coroutine, that's it). It would've been better to have them from the get-go thou.

The big elephant in the room

Ok, enough gushing we need to talk about the one-shaped elephant in the room, or more precisely - one-indexed arrays. Look, I know. It's weird. I was put off from learning lua at first just because of this, but after that, I realized that 1. it literally takes you 1 month to adjust and 2. we should've been programming like this from the get-go.

Now, put down your pitchforks, put the guillotine back in the 1800s, and hear me out. With 0-indexed arrays you constantly need to think about the fact that index ranges are start-inclusive and end-exclusive. This can lead even the best developers to make stupid one-off-errors. One-based indexing on the other hand provides us with start and end inclusive ranges. This means that when you do string.sub("123456789", 5, 7), you will get "567", and nothing weird. At first, it will throw you off, but it really grew on me and I wouldn't like it any other way.

The other big elephant in the room

Ok, where do these elephants keep coming from? In all seriousness, we need to talk about nil. Now, this is where our problems start.

Now, let's get a little bit theoretical and dare I say, philosophical. It's a very hard (and I would ever go as far as to say impossible) task to implement the "void" value (null, undefined, nil, none, call it whatever) in a sensible way. It is basically a game of deciding how much you will scream at you user when he tries to use void values. When you have too many errors, the whole language becomes stiff and rigid, but you can catch the inception of a faulty void value earlier. On the other hand, you can never throw errors when you work with void values - want to get a member of nil? It's nil! Want to print nil? Sure, go ahead! Want to concatenate nil to a file pointer? It ain't illegal, so I don't see a problem! Of course, the truth is somewhere in the middle and there is never a right answer.

Lua however seems to have gone down the JS school of thought (2 years... before... the inception of JS) of not screaming at the programmer when you try to get an inexistent field. This has the unpleasant effect of not being screamed at when you misspell a variable name, because operations with variables that don't reference locals are automatically coerced into operating with globals, and globals are just fields of the global _ENV object. This means that you basically need a linter or an IDE to work on bigger Lua projects, because one misspelling and it's game over.

Another place where nils fall apart are in arrays. Now, since arrays are just tables that have integer fields from 1 to n, we need to somehow imply where the end of the array is. In Lua, by convention, that is the first nil value. As you can probably guess, this directly means that a nil in the middle of your array basically truncates the rest of the array (almost like \0 in C strings, another treacherous idea). I hope I don't need to explain the implications of this, but I will anyways, just to drive the point home:

Of course, nil doesn't behave with tables as well (gosh, what an antisocial freak). In tables, actually, you can't have nil fields. When you do obj[key] = nil, what you're actually doing is deleting the darn field. This, of course, has some nasty implications again:

In all honesty, it seems that the Lua authors got a little bit too clever for their own good. For arrays, just having a special-case table would've been completely fine. Sometimes, sacrificing theoretical cleanliness is worth it if a better developer experience is achieved at the end. And for the tables, you can just throw errors when you try to access inexistent fields, have a function like has(table, key), and allow having nil fields.

The bad...

Let's cut to the chase: there are good reasons Lua never hit the mainstream properly. The only reason you've heard of Lua (if you have at all) is 1. this post and 2. having to use it for a program that happened to embed it. Yes, it is a really easily embeddable language, but this means that a lot of tradeoffs have to be made. The language needs to have a small footprint, which in practice means that a lot of the bells and whistles (the stdlib, the concurrency model, networking, UI and a lot more) are left as an "exercise for the reader".

This of course is a valid approach, but it pretty much dooms your language to be a second-class citizen in the minds of all of its users. It becomes just the means of making a piece of software do stuff it didn't do before. This is cool in its own right, but handicapping your language in such a way is stupid to say the least and just puts a glass ceiling for what your language can be used for.

The first terrible thing you've probably experienced from Lua is the built-in CLI. As a reference implementation it might be fine, but it really gives you the "Really? That's it?" impression, and Lua has SO MUCH more to offer. You don't get any autocomplete, you don't get any command history between sessions, the REPL just prints what comes from tostring, which for tables looks like table: 0x6553c074f7f0 (EEK!). If there's one thing I hate with a passion about Lua, it's the REPL (which is such a good problem to have, because you can literally make your own).

Next, the module system, although simplistic, is a crime against nature. I like the idea of the dot-separated notation of "path.to.my.module", but WHY in the FUCKING HELL are imports relative to the CWD, and NOT the current (or main) module. I'll tell you why. It is to make the module system "more simplistic". In this way, you have one deterministic way a module can resolve, and you can't use two strings to refer to one module. This makes the model of module resolution and caching very simple. However, trying to develop anything with more than 3 files using this scheme can prove to be... cumbersome.

Another cumbersome thing is the stdlib. It really shines... with the 5 functions it has. Like, is that really all? I truly believed that I would never live to see a language that didn't have fucking string.split, but yet here we are. Instead of writing the objectively more appealing for val in str:split "," do ..., we have to write for val in str:gmatch "[^%,]" do ..., and it really works only for single-character delimiters. For anything more complex you have to basically implement your own split function.

Split isn't the only function, the absence of which is painfully obvious thou. On the top of my head, the following utilities are nowhere to be seen:

Of course, you can implement or substitute these missing functions yourself, but it would've literally costed nothing the Lua peeps to implement a more comprehensive standard library.

The root of the problem / Semi-Conclusion

I think that what holds Lua back is it's authors' mindset - completely disregard anything the community actually wants from the language. Of course, the authors of any language can feel free to develop the language without taking the community into account, but in such cases, you just won't have a community. PUC has been known to break compatibility with basically every new release (binary compatibility, too), without a good reason to do so. Currently, one of the best runtimes for lua, LuaJIT, is stuck between 5.1 and 5.2, having binary compatibility with 5.1, but supporting a few 5.2 features, because of the split in the community that 5.2, 5.3 and 5.4 caused. For all intents and purposes, PUC has created a separate community for each version.

Lua is a great language and it's simplicity and architectural soundness really make it an enjoyable language to use. However, the stubbornness of PUC has doomed the language to be the weird one-indexed language that you have to use to configure this piece of software. I wish more people would give the language the time of the day (and not other pseudo-wanna-be-make-believe languages), but alas, we can't have nice shit.

PS: Srry for making the post so atrociously long, there is just a lot to be said about Lua

Comments:

By anonymous
#739840
2025-02-13 09:54:06
no comments :(