Sadly, MoonScript could be better (and where Lua fails)

Published: Thu, November 27th, 2025
Last edited: Fri, November 28th, 2025

Contents


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.

libraries

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.

enter MoonScript

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?

the missteps

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!

let’s go over it

keep it

  • variables are local by default, global with 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?
  • update assignments: a small but welcome addition. tactfully, ++ 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 interpolation: very nice to have! string.format isn’t bad, but this makes it quicker and clearer and easier. especially useful for debugging.
  • argument defaults: helpful, especially because Lua doesn’t have a concise way to check for 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.

  • arrow syntax for function literals ->: 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.
  • fat arrows =>: 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.
  • comprehensions: i think this adds a lot to Lua, since you’re constantly operating on entire tables. can also be used for quick shallow copies!
  • * 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 ^?
  • slicing: helps with comprehensions, but otherwise i haven’t used it much.
  • 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.

  • object orientation: this is a big one. now, Lua’s general-purpose enough to handle OOP adequately, and it wasn’t quite as big of a paradigm back in the 2000s, so i don’t blame it for not having first-class structures for it. but it feels good to have a regular-ass 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.

drop it

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.
  • function stubs: being able to pass around a function bound to an object IS fun, but in my opinion, at that point you should just make a dedicated table for holding a class-function pair, or use a 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.

  • multiline arguments: stupid. required scaffolding for the parenthesesless function calls.
  • : 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.

  • implicit 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.
  • parentheses for function calls are optional: noooooo. this makes it SO much harder to scan for functions. and you use them everywhere, so MoonScript code ends up looking like keyword slurry! especially when combined with this next one:
  • curly braces for tables are optional: what a truly terrible idea. you save yourself two (2) keystrokes, and gain the ability to
 make it harder to parse when a table begins and ends. god help you if you’re using tables inside function calls (the first argument in print bools: true, true has 1 field). is this easy on the compiler or something? a prank? it’s hard to tell.
  • assignment inside 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.
  • line decorators / if inversion: just makes code harder to read for no benefit. i think Leafo was just playing with possibilities with this one.
  • implicit return on files: i don’t like implicit returns man. you won’t make me like them. Rust has them and i don’t like that either. i get that you want to be like object oriented languages, where 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?

results

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.

conclusion

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.