my last blogpost was well-researched and approachable so iâd like to take a candid moment to whinge about some niche shit. please take this post lightly, because itâs some very personal gripes that youâd only get from using Lua A LOT, and iâm still Lua fan #1. so.
itâs no surprise i like Lua. for someone whoâs admittedly pretty bad at following complex logic, its simple syntax keeps things in check; you bump against the skill ceiling fairly early (compared to other languages) and so are stopped from nearly-infinitely rethinking and overcomplicating your codebase. what a shame it is then, that writing it gets laborious so quickly! between a minimal standard library and little syntactic sugar, you end up forced to write a good deal of boilerplate for even simple operations.
this makes sense. itâs designed to be small and embeddable, so it goes for a straightforward and easily-parseable syntax. (you can even write everything in one line!) it works wonders for that, but for all the usecases where it stands on its own, like LĂVE or Lapis, i find myself wishing it just⊠got out of my way sometimes.
the keywords are just the beginning. then and end are annoying - and remembering the difference between then and do even more so! - but the lack of tooling for them makes it even worse! at least my editor knows i always want a } when i type {, and that hitting Enter means i probably want these two tokens spaced and indented in a certain way. but trying to wring automatic end insertion out of an editor is a task and a half, doubly so for indenting it properly, triply so without messing with snippets.
at least it doesnât have semicolons. yuck.
unfortunately, the widely-used Lua 5.1 came out in 2006, before a lot of modern programming practices were codified. one such is that people realized years ago that functional programming is a helpful paradigm, and that arrow-syntax is better than typing out function every time. i sorely miss it in Lua, and it always makes me just sliiiighly less likely to spring for a functional solution to an issue.
iâd also complain about the lack of proper OOP support, but honestly? the fact that it ships with all the building blocks for a pretty decent object system, and that you can just use tables and functions a lot of the time, is refreshing and liberating. itâs a big strength.
âŠthe issue is having to type out MyClass = Object:extend() (or, god forbid, MyClass = {}; MyClass.__index = MyClass - thatâs three times you have to write out the exact same name!) and then function MyClass:my_func() for every single method. torture!!
from Lua:
local MyClass = {}
MyClass.__index = MyClass
function MyClass:my_func()
...
end
return MyClass
to Python:
class MyClass:
def my_func(self):
...
thereâs an about 50-character difference in how much you have to write. which isnât that much, but with 10 classes? 50? it adds up to Lua constantly feeling just a little more bothersome to write in.
yes, âbut donât libraries help with that? Lua has such a good module system,â i hear you say. it does! you can get off the ground much quicker with some great extensions to the standard library like batteries and Moses. as a point of comparison, letâs process a table into only the squares of its even values in pure Lua:
local numbers = {1, 2, 3, 4, 5, 6}
local processed_numbers = {}
for i, num in ipairs(numbers) do
if num % 2 == 0 then
processed_numbers[i] = num * num
end
end
and with Moses:
local M = require("moses")
numbers = M.chain(numbers)
:filter(function (n) return n % 2 end)
:map(function (n) return n * n end)
so much better! even printing is way easier:
M.each(numbers, print)
however, did you notice how much repeated code still exists in the Moses version?
numbers = M.chain(numbers)
:filter(function (n) return n % 2 end)
-- ~~~~~~~~~~ ~~~~~~~~ ~~~
:map(function (n) return n * n end)
-- ~~~~~~~~~~ ~~~~~~~~ ~~~
21 characters out of the 38-character-long line are boilerplate that we donât care about - more than 50%! compare with something like JavaScriptâs arrow syntax:
numbers = numbers
.filter(n => n % 2)
// ~~
.map(n => n * n);
// ~~
it looks even worse compared to Python.
numbers = [n * n for n in numbers when n % 2 == 0]
my point with all this isnât that this makes Python good and Lua bad, but to show that thereâs some common approaches thatâre bothersome to write in Lua, even with helpful libraries, because the language just isnât built with them in mind.
MoonScript is a language that compiles to Lua, made by Leafo (the guy behind itch.io). itâs greatly inspired by CoffeeScript, meaning lots and lots of syntactic sugar. most notably, it does away with block denominators and replaces them with significant whitespace.
numbers = {}
processed_numbers = {}
for i, num in ipairs(t)
if num % 2 == 0
processed_numbers[i] = num * num
i know, the horror! but weâve all gone through this song and dance before and my stance is that programming languages should benefit the programmer, and i scan for indents more than count brackets when skimming through a piece of code, ergo significant whitespace doesnât bother me.
it also uses an arrow syntax instead of function. the Moses example can now be rewritten like this:
numbers = M.chain(numbers)\filter((n) -> n % 2)\map((n) -> n * n)
except you donât even need Moses, because MoonScript just has comprehensions!
numbers = [n * 2 for n in *numbers when n % 2 == 0]
hooray!
so problem solved, right?
but Leafo went too far. drunk with power and armed with LPeg, he dared to crunch the syntax down to an extent never seen before, crossing the line from âconciseâ into âobtuse.â
MoonScript adds MUCH more syntactic sugar than this. that is its vice. you actually have to purposefully avoid most of it to maintain readability.
letâs see a more involved example, styled after how Leafo himself writes MoonScript:
flatten = (t) ->
res = switch type t
when 'table'
result = {}
for v in *t do insert_many result, flatten v
unpack result
else
t
res
this code only really âfolds outâ into a simple procedure once you take away the implicit returns, optional parentheses, and conditional assignment.
flatten = (t) ->
if type(t) == "table"
result = {}
for v in *t
insert_many(result, flatten(v))
return result
else
return t
it doesnât just end here. each feature added by MoonScript can make the code feel arcane, inscrutable, demonic - with what seems like a nearly-50% hit rate. and iâve used this language extensively before.
so letâs travel through every. single. thing. this language adds. and see which parts earn their keep!
export: yes! a great step in the right direction. however, i miss local as an explicit declaration keyword; implicit declarations make it hard to scan for the first use of a variable, so itâs hard to see shadowings. MoonScript actually introduces an entirely new construct - using directives - to combat this extremely self-inflicted problem! on top of that, export makes it sound like itâs only for the current module - global mightâve been better?++ and -- are nowhere to be found, thank god. (maybe unavoidable; -- starts a Lua comment.)!= as an alias for ~=: yes, this shouldâve been in Lua. ~= was a product of its time, maybe based on the bitwise NOT.string.format isnât bad, but this makes it quicker and clearer and easier. especially useful for debugging.nil. the or operator can work as a replacement, but fails with bools.so far, so good! these are all very welcome improvements that give MoonScript a bit of a âmaturityâ that Lua doesnât have, like youâre finally stretching your legs and writing something that flirts a bit with QoL.
->: wonderful. Lua has first-class functions, and theyâre used in a bunch of different places, so having to type function() end EVERY TIME contributes a lot to its noise. especially if youâre doing functional-style programming.=>: this is a cute way to include an implicit self, but hard to notice and has bitten my ass multiple times before. iâm not sure how iâd fix it.* operator for numerical iteration: iâm a fan of it, especially because i always forget to write ipairs() when i want to quickly hop through a table. would an equivalent pairs() operator be nice? maybe ^?continue statement: a big blind spot of Lua is its lack of continue, which means you have to use goto (not even available before 5.1!) or complicate your control flow to get the same effect. this is a no-brainer - if Lua has break, continue wouldnâtâve hurt.all of these help dearly in giving Luaâs sexy, functional side some more room to breathe. smaller function literals make it painless to create new functions; comprehensions and * and slicing make it buttery smooth to operate over tables.
class keyword.@ as an alias for self: yes, writing self every single timeâs a chore. this is the next best thing after an implicit self.@@ as an alias for self.__class: sure, whatever. âŠhey, if thereâs super, then why not a new keyword to refer to the current class, static? or drop super and just use @@? âŠitâs not a big deal.import x, y, z from A: Luaâs require syntax is fine, but unwieldy if you want to import more than one field as a local. this helps make imports more explicit, and discourages you from importing an entire module if you wonât need it.these ones bring some more s-OOP-y flavor, which may or may not be your jam, but i think âbundles of data and behaviorâ are fundamental enough to warrant their own keyword.
, after every line anyways, might as well ratify the common convention of newline = new table entry.this is where i draw the line in the sand. itâs where, i feel, MoonScript trips over into syntax thatâs ambiguous, unhelpful, or even dangerous.
switch: works well with conditionals as expressions, which is fun, but iâm not too bothered by writing ifs, so i personally could take it or leave it.with: another thing thatâs pretty handy in an unfortunately small amount of situations. chaining anything you want is very nice, but i havenât found the complexity overhead worth the benefit.bind()-y thing to glue them together.unless: huh, a holdover from CoffeeScript? which in itself is a holdover from Perl? itâs not that hard to negate conditions, you donât need a new keyword dedicated to making you run through the logic in your head twice over to make sure you get it.if statements as expressions: makes for a pretty ternary, but overall it adds complexity without the benefit being that crazy.all relatively harmless, but when given the option to have something that might make something easier to write? maybe? in some cases? i end up just not wanting the option.
: instead of = for table fields: iâm⊠not sure why this is here! i donât see anything wrong with using = for tables, but : is already widely associated with dictionary-style data structures, so this is overall a pretty lateral change.: prefix shorthand for table fields with the same name as variables: fine for long variables, but i donât really care.\ instead of : for method calls: wait, what? why? oh god, is it cause : would collide with table fields? but i wasnât even asking for that! this is so ugly. by godâs hooks have the decency of making it :: at least.export * and export ^: uh, why? do people declare globals THAT often? apparently. i think you could just write export each time and just make it clear.using clause: a MoonScript construct that remedies an issue MoonScript introduces. just donât introduce the issue! this wouldnât even be a consideration if MoonScript just had a variable declaration keyword, and itâs too niche and obscure to make it worth having. get rid of it and the underlying issue.also all relatively harmless, but also just kinda slightly smelly, befuddling design decisions. like why would someone add this? i think they technically count as QoL features, but theyâre so weirdly intermixed and overlapping in a way that should make you stop, think for a second, and start untangling these intertwined issues.
hereâs where it gets dangerous.
return: a huge and naive mistake. implicit returns completely hide the exit points of a function, and whenever a function ends with a function call, itâs ambiguous whether its return is intended to be used or not. furthermore, Lua has optimized tail calls, so in MoonScript, EVERYTHING is a tail call, and they decimate the stack trace. iâve been there before. itâs not fun to debug. this is a slew of issues for the benefit of saving a few keystrokes. just type return. jesus.print bools: true, true has 1 field). is this easy on the compiler or something? a prank? itâs hard to tell.if checks: i can ssssssseeee the intent behind it, but itâs such a small and common mistake that iâd rather it error than silently do something different. at least with C# you have to declare it with var.! as an alias for (): no one was complaining about this. makes it harder to scan for functions, and adds a slight mental overhead. saves One (1) character. bad feature.if inversion: just makes code harder to read for no benefit. i think Leafo was just playing with possibilities with this one.class defines a class in the current namespace, but just writing return class X wouldâve matched up whatâs actually going under the hood way better.these are all capital B Bad. they go beyond âjust a different way to write it:â their existence makes the whole language worse, theyâre pitfalls waiting for you to fall into. i can only comprehend their addition to the language from the perspective of someone who really, really doesnât like typing and wants to minimize it as much as possible when programming. itâs an exercise in how much of a language you can make implicit before it implodes. by god.
i think a recurring theme here is that i like it when my tech is opinionated. if MoonScript just made all this stuff mandatory, iâd be able to say âwow, itâs assâ and go use something else. iâd comfortably disagree with the author. if it was committed to its own quirkiness, you could think âwell, someone went to the trouble of making it, so it must be really good for them. just not for me.â but it adds a huge amount of extraneous features, and makes all of them optional! so i feel like iâm being told âyeah, thereâs just a bunch of random stuff in here. but if you donât like it, you can just not use any of it! thatâs flexible design, right? you can do everything in so many ways!â
but i donât feel reassured that any of these ways are curated or preferential! and when you lay them out like this, theyâre clearly not!
so, some good, and some bad. what happens when we tally it up?
out of 35 new features MoonScript introduces, i dislike 17. 48% - thatâs nearly a perfectly even miss rate. impressive!
i only have such strong opinions on MoonScriptâs syntax because iâve liked it enough to use it extensively; it pains my heart that it missteps so often and results in an all-around weak dialect of Lua. iâve seen people defend it, and i can only ever join in reluctantly. itâd make my day to be able to recommend it wholeheartedly, or to be able to use it without an extensive style guide that bans nearly half of the new additions.
iâve even considered doing it my damn self and making my own fork that rips out as much of the gunk as i can muster, but iâve never touched a compiler, and if itâs SO HARD to even install Luarocks on Windows, i donât want to know how hard it is to develop for the thing.
jesus christ, i didnât even mention how hard it is to install it on Windows.
so what would my âperfectâ programming language look like? itâs hard to think about it abstractly because most languages are fit for their domain. but a theoretical general-purpose language iâd see myself using the most would look pretty close to Lua - maybe just with terser syntax and better quality-of-life features.
well, other people mustâve agreed with me in that regard, because thereâs so many âLua-yâ languages out there! Squirrel mentions Lua in its landing page. Wren calls itself âLua-sized.â Fennel is a dialect of Lisp that compiles down to Lua - how cool is that? it seems a shame to me that Lua is old (or hard to extend?) enough that it feels restrained by the era it was made in.
huh? oh well, yeah itâs still getting updates, but they feel weirdly small and inconsequential - integer types, bitwise operators, goto - and the ecosystem doesnât seem very interested in catching up. LuaJIT is based on 5.1, Robloxâs Luau is based on 5.1âŠ
some Lua libraries havenât been updated in 10 years and are still considered state-of-the-art. this is kinda rad! itâs a very unique situation for a piece of tech to find itself in! Lua is solid enough that we can focus on improving and evolving it ourselves, instead of playing catch-up with official updates. weâre allowed to just have a good, straightforward, reliable tool that probably wonât change. tutorials donât get outdated, libraries donât collect dust. and it still has a pretty loyal and affectionate following. itâs⊠honestly pretty nice.
as for MoonScript itself⊠(deflated sigh) itâs not that serious. itâs a beta of a language made by one guy a decade ago. i think i just needed to get all of this out of my chest, because i tried making a whole game in it, and it eventually made me pull my hair out.
whew.