Unfortunately, I have to agree. It’s got great runtime semantics: lexical scope, proper closures, real first-class functions and tail-calls, and the way it handles closures internally is quite elegant. Most importantly for an embedding language, it’s got a really minimal build system that makes it really easy to integrate and its C API is quite nice. But the language has a lot of footguns, some of which can be avoided with better surface syntax. I usually reach for fennel.
You might be interested in Janet. The author is still Calvin Rose (the guy who wrote fennel), the VM and runtime is reminiscent of Lua’s (I find the C API to be much better, and the fiber concept is an interesting alternative to lua’s coroutine implementation), while being much less footgun-y as a language.
I’m curious, what did you find to be better about the C API? In my experience it was a lot more difficult to drive Janet from C than driving Lua. It did seem to handle the other direction pretty well though.
My use-cases tend to be the other direction, indeed: I wrote Jurl and janet-date (http client, I’m actually quite proud of the high level API design, even if the internal implementation could use some work, and proper datetime support).
Lua’s stack juggling when writing modules, lack of strong support for documentation and defining sources, and how you have to coerce the types to do what you want isn’t great.
Janet’s abstract types are a really great way to encapsulate C objects in a way that feels native (in Janet), and the small but capable internals library means that you get to skip some of the hardest work in writing bindings (argument validation).
Anyway yeah, I admit I haven’t actually embedded Janet all that often (most cases thereof for me are jpm-built CLIs, so Janet is still the driver in a lot of ways) so I don’t have strong opinions in that direction.
To each their own, I happen to really love it. Not gonna try to convince you otherwise, but would love to know what scripting/embeddable language you prefer. Not picking a fight or flamewar, just curious. Besides Lua, I enjoy Janet a lot.
Lua is is not perfect but it’s not terrible either.
The fact that a neat little Lisp like Fennel maps so nicely onto Lua precludes it from being terrible. It’s also too small to be terrible. You need a larger language surface to have a chance at being truly terrible.
I’ve never been drawn to Lua, but I really like Go–in particular, it’s minimalism and simplicity makes it really easy to get stuff done. If Lua is Go-like, I suppose I should give it a second look.
For what its worth I enjoy both Go and Lua and for similar reasons: both are languages that fit in my head, not too much syntax or magic, the code “does what it says” for the most part. Easy to read other people’s code.
Yeah… Another big wart: variables being global by default.
The local variable attribute (local x <const>) in Lua 5.4 is kinda strange.
Using doubles as ints, then introducing 64-bit integer types in Lua 5.3 seems to introduce a lot of optimization hurdles. It’s nor favored by the LuaJIT creator.
You can have projects written in language you don’t like? Like it’s more believable when someone has maintained many Go projects than not, if it comes to opinion if language is good.
Re indexing for arrays, the author says “In Lua, indexing generally starts at index 1, but is only a convention. Arrays can be indexed by 0, negative numbers, or any other value (anything but nil).” I agree and disagree with “only a convention.”
Yes, it’s only a convention in the sense that a Lua interpreter will not throw an error if you create an array-like table starting from 0 (or some other number). But, no, it’s not only a convention in the sense that the standard library for tables and built-ins like ipairs assume array-like tables with indexes starting at 1. So, for nearly all practical purposes, you probably want to index your array-like tables starting at 1. (As Programming in Lua says, “The Lua libraries adhere to th[e] convention [that array-like tables start at 1]; so, if your arrays also start with 1, you will be able to use their functions directly.”)
foo = { [0] = "fizz", [1] = "buzz" }
for _, word in ipairs(foo) do
print(word)
end
-- Prints only "buzz"
A similar point applies to what the author says about nil-terminated arrays. If you know (or suspect or worry) that you may have a sparse array, you must not use ipairs to loop over that table. Instead, you can use pairs (or next) to get at the whole set of items without stopping at the first nil. (If the order matters, however, you’d have to save the items in a new table and sort that table before use.)
Maybe the bigger point is that Lua does not really have arrays in the sense of sequences. There’s just tables, and the tables are always key-value hashes. Sometimes the keys are integers, but—by a (strong!) convention—the indexing of array-like tables should start at 1 and usually the sequence shouldn’t have any gaps.
Maybe the bigger point is that Lua does not really have arrays in the sense of sequences. There’s just tables, and the tables are always key-value hashes. Sometimes the keys are integers […]
I think this is a pretty good way to put it. Another way to say it is that an array is not a data type; it’s a way of treating a table.
You can take any table whether it’s an array or not, and do array things to it. This will lead to surprising results IF you assume that nil in Lua behaves like nil in other languages; it takes some time to internalize Lua’s meaning of nil as the genuine absence of value. But once you do, it’s a good deal more consistent than how many other languages treat nil.
This is true of almost every feature of Lua that people complain about; it’s not that it’s bad, but it’s frustrating if you come to it assuming it works like other languages.
I’ve seen photos of it, but never bothered to learn more. Thanks for mentioning it, it looks pretty cool! Surprised to see it was made by Panic, who are (at least to me) known for macOS&iOS dev-specific apps and utilities.
UPD: Oh, apparently Panic is also a video game publisher! One of the games I really enjoyed was Firewatch, which TIL was published by them. Untitled Goose Game is also fun.
LÖVE expects these files exists and panics (calls error) if they don’t.
This is fine because in a game, you should be able to expect all the required files exist. If you still want to handle the error, you can use pcall as shown in the blog post.
Another thing that I love about Lua is its coroutine implementation. It’s a very elegant implementation that composes different coroutines well. You don’t need to remember to yield to an inner coroutine like in python, it just nests properly and yields as needed. Return values also work like you’d expect. E.g.
function g()
coroutine.yield(3)
end
co = coroutine.create(function()
coroutine.yield(1)
coroutine.yield(2)
g()
return 4 -- shows up in the stream like you'd expect
end)
while coroutine.status(co) ~= "dead" do
local status, value = coroutine.resume(co)
print(value) -- prints 1, then 2, then 3, then 4
end
This stuff is possible in other languages, but it feels more bolted on and more kludgy. With Lua, you write the straightforward cooperative threading logic, and it works as expected. I think that’s great :)
Maybe I’m too used to other languages, but that mostly works the opposite of how I’d expect! The idea that any function can call yield, and if it happens to be called by a coroutine it’ll yield feels pretty wild. What happens if you call g outside of a coroutine? And is there a way to delegate to another coroutine?
This feels like really dynamic effects, which is clever, but I wouldn’t want to have to be in charge of keeping track of.
The idea that any function can call yield, and if it happens to be called by a coroutine it’ll yield feels pretty wild
It really opens up a lot of possibilities that you don’t have in languages with stackless coroutines, because it means that you can turn any code into non-blocking code even if it wasn’t written with non-blocking behavior in mind.
I’ve used it before to integrate a REPL into a GUI where the REPL was not designed to yield, but coroutines made it really easy to integrate into the GUI’s event loop anyway: https://wiki.fennel-lang.org/Repl Never could have done that with stackless coroutines!
What happens if you call g outside of a coroutine?
You get an exception/stack trace. I’m unsure (just from lack of trying) if any static analysis tools or LSP implementations can catch stuff like this, or if you just have to let it blow up at runtime like in my REPL example here.
function g()
coroutine.yield(3)
end
g()
attempt to yield from outside a coroutine
stack traceback:
[C]: in function 'yield'
stdin:2: in function 'g'
stdin:1: in main chunk
[C]: in ?
I’m not a fan of languages that can’t detect misspelled variables at parse time. Perl was my first dynamic language, and I was dismayed that other dynamic languages are worse at detecting typos.
Lua’s 1-based indexing hasn’t bothered me, and I’m saying this as a long time C programmer (35 years, and before that, assembly programmer), but that could be just me. In fact, I recall having a harder time going from assembly to C as there were things I could do easily in assembly that were hard (at the time) in C (my biggest complaint at the time—the inability to return multiple values easily).
I wouldn’t really say that they disagree, so much as that they both can work in ways that are surprising to people coming from other languages or with specific expectations. But maybe I’m misunderstanding what you mean. Can you give an example of the disagreement you have in mind?
for index,value in ipairs(table_variable) do
...
end
It works by starting at 1, and continuing until an index returns nil. # is the length operator, and for tables, it does a binary search to find the ending index (seriously). So as an example, if you had the array { true , true , nil , true , true , true , true , true } (element at index [3] is nil) then it could return 8, or maybe 2, depending on how the binary search jumps around.
Arrays in Lua have always worked this way (as far as I can tell). The terminology around this is a bit strange, but Lua calls what we would call an “array” a “sequence”—a table has elements at array indexes starting at 1 and each subsequent index increases by a whole number and ends with an index returns nil (marking no entry; as an optimization, it will keep such a sequence in an actual array, but it has to be constructed pretty much in order for that to happen). The length of such a sequence is, as stated, found by a binary search.
One consequence of this is the inability to determine the difference between “undefined” and “no value”. In other words, every table has a “value” for every key you could imagine; it’s just that most of the values are nil. A nil in a sequence: { 'a' , 'b' , nil , 'd' , 'e' } is undefined by the Lua language semantics. Enough people complained about this that the team added false to address this issue (and added true to have an actual Boolean value). The only two values in Lua that are treated as false in a Boolean sense are nil and false (such that if 0 is considered true because the value exists). If you want a nil (logically, not physically) in a sequence, you are supposed to use false instead, as that will keep the sequence legal in the language sense. { 'a' , 'b' , false , 'd' , 'e'} is a valid sequence of length 5. Before using such a value, you use if value.
As the documentation that I linked to says, #t finds an arbitrary border in the array part of the table, and as the article says, ipairs(t) finds the first.
As the documentation that I linked to says, #t finds an arbitrary border in the array part of the table, and as the article says, ipairs(t) finds the first.
Got it. I understand what you and spc are saying, but I don’t see this as a disagreement. As I understand the documentation, neither ipairs nor # offers an answer to the question “What is the length of a table with nils in it?” That doesn’t seem unreasonable to me since the question does not have a single, unambiguous answer. In any case, thanks for clarifying.
As I understand the documentation, neither ipairs nor # offers an answer to the question “What is the length of a table with nils in it?” That doesn’t seem unreasonable to me since the question does not have a single, unambiguous answer.
It’s not that it’s an ambiguous question; it’s more like an impossible question. Asking for the length of a table with nils in it is like asking how many nothings you can fit in a bag before it gets full; the question itself is kind of nonsensical given Lua’s definition of nil and table.
I’m not a fan of languages that can’t detect misspelled variables at parse time. Perl was my first dynamic language, and I was dismayed that other dynamic languages are worse at detecting typos.
Honestly this used to be a much bigger problem before LSP became a thing; nowadays it’s very easy to detect mistyped identifiers as you type. (Even without LSP it’s easy to catch with luacheck but before LSP that required a separate manual step.)
I”ve been using Lua quite a bit recently. Sol3 makes it incredibly easy to embed from C++, you can call cast Lua functions to std::function, expose functions to Lua by just assigning them to indexed elements in the Lua VM wrapper, and expose entire class types by just passing method names and pointers (or lambdas if you want the Lua version to be a lightly sugared version of the underlying C++ one). It transparently converts Lua tables to C++ collection types (anything with iterators). Unique and shared pointers inter operate with Lua memory management, with the destructors being called from the GC.
100% agree. I embedded Lua into my hobby 2D game engine and with macros and a bit of template magic to unroll and pass function parameters and return values as appropriate types it’s absolutely magical to be able to bind things from C++ with only a few lines.
Not mentioned in the article is the amazing hooks Lua provides to be able to examine the state call stack, profile, and other options. I’m able to generate incredible script error reports with not much more than the built-in capabilities.
I posted in another thread, but if you haven’t already used it I’d recommend taking a look at Sol3 it provides a lot of the template magic to do very nice Lua bindings from C++. It also abstracts over various Lua implementations so it’s trivial to swap them without needing to modify your binding code.
I love Lua. My favorite thing is that being a small language, I don’t spend a lot of time thinking about the right way to do something. If I can bang together a loop over a table to do my computation that’s good enough.
One interesting Lua application out there is Path of Building, a sort of theorycrafting simulator for the game Path of Exile. It’s a giant complex pile of Lua code with many outside contributions. And has an intensive GUI too. It works just fine.
I’ve been enjoying lua-for-configuration in Neovim and Wezterm recently while setting up a new computer. Especially in Neovim, it’s so great to go from the very quirky and weird Vimscript config to Lua where a setting looks like a setting, and a function call looks like a function call. More or less.
Neovim and many modern written-for-nvim plugins also sport great type documentation coverage, so you can get great completion and in-editor type checking… once you get your config running and install the lua checker.
For general programming I find the situation with array/list/sequence “nil termination” unacceptable. It’s bad enough in C strings but to have the same kind of wonky issue for the basic “ordered compound type” in a dynamic language is just… ugh. It will hide nil pointer mistakes and instead turn them into silent data loss, horrendous. I guess people deal with it because large amounts of production lua do exist, and there is the type checker stuff. I just don’t want to have to think about such things when programming.
I have used both and i cant say i enjoy using lua at all. Instead, i would prefer something simpler, preferably with typing support for better ide/lsp support. In my experience, buck2 starlark got it just right.
My favorite part of Lua is that it has so many compile-to-Lua options (similar to JavaScript) which means you can choose something you personally find more ergonomic.
Lua is a language I’d recommend anyone check out, and I’m glad to see an article showcasing some of its coolness. That said, I don’t know if I’d recommend it for anyone’s primary programming language. (Not to say that it shouldn’t be!) I’ve used it for configuring WezTerm, as well as some fun coding adventures in Minecraft through ComputerCraft: Tweaked and similar mods. At one point, I even was working on a Twitch chat display for fun.
It’s got a lot of power, and its proliferation definitely comes from its ease of embedding. Though it’s a language that, to me, always feels foreign. Coming from mostly JS/TS, and everything feels just a little “off” when working with it, but the language is one that’s probably easiest for me to hold in my mind while I’m using it. And there are some projects like Moonscript that transpile to Lua that can make it feel a bit more familiar.
I’m hoping to build up enough interest in a game development class to pilot a course using Lua. We shall see. If it happens though there are a lot of opportunities for learning that I’m really excited about.
Tables as the only container type in Lua is something really interesting to me. It’s like the cons pair in Lisp. There’s some tantric superpower in the simplicity of a network of tables that enlace.
I really feel like JS and PHP have caused people to view “one data structure can act as a key/value dictionary as well as a linear sequence” with suspicion, but yes, Lua shows that it’s possible to do well.
Yes. It took a long time for me to get used to it, but at this point it feels so clunky when I go back.
Most languages just have so much unnecessary confusion over “does this hashmap actually have nil at this key, or is it absent altogether?” which is just … an entire class of problems that does not need to exist.
Most languages just have so much unnecessary confusion over “does this hashmap actually have nil at this key, or is it absent altogether?” which is just … an entire class of problems that does not need to exist.
Stop using bad languages :p
Treating your “no value” value as actually no value consistently is better than being inconsistent with it, but it’s worse than simply not having such a thing in the first place. print(foo) when foo hasn’t been assigned to should be illegal, not print nil.
print(foo) when foo hasn’t been assigned to should be illegal, not print nil.
Yeah, so… I’m literally the lead developer of a compiles-to-Lua language that makes this a compile error like you say. But that is completely separate from how nil behaves inside data structures.
Overall I’m not a fan of nil, and I would prefer the approach of a language like Erlang which omits it entirely over the way (for example) Clojure or Ruby handles nil. But I have a hard time seeing how that could be viable in Lua without compromising on the ruthless simplicity that is Lua’s primary design goal.
empty = {}
map[key] = empty
if map[key] == empty then
-- do something
end
equality on an table is guaranteed to be uniquely defined for that table only. it’s not a particularly hard problem, and I’m sure you know this too. You get the choice to do it that way if you want, it’s just not built into the language.
Lua, like C, is pretty minimal at its core, but all of the functionality is there for you to implement if you want. I think the thing people don’t like about Lua is that they have to program and make decisions about things and there’s not a batteries-included-opinionated-make-the-decision-for-you default, which I personally don’t mind.
I have my own Lua and my own style.
Also a fun thing I discovered the other day, since I remember you from my neovim days, and you might appreciate it.
local function f(s) return s end
print(f"%q %d":format("hello", 123))
It’s funny how you can’t do "%q":format without doing ("%q"):format, but if you just prefix it with a function call, the parsing works out fine :P
And even better, the type system lines up so well with a certain other language that you can use TypeScript to write it: https://typescripttolua.github.io/
So it feels like you’d end up writing TypeScript that is so dependent on Lua quirks that you might as well work with the language and write Lua instead.
I don’t mean the nitty-gritty of the type system like == equality and null vs undefined etc., but rather that the primitives are mostly the same (Lua even treats numbers as floats, like JS!), and you make compound types by using objects/dicts/tables, and arrays are just a special kind of those.
They even say:
TypeScriptToLua aims to keep identical behavior as long as sane TypeScript is used: if JavaScript-specific quirks are used, behavior might differ.
I used it for an ad-hoc NeoVim extension once and found it worked pretty well.
Lua is overrated. I’m always disappointed by it. It reminds me of Go in having a cool underlying runtime/interop but a terrible language on top.
Unfortunately, I have to agree. It’s got great runtime semantics: lexical scope, proper closures, real first-class functions and tail-calls, and the way it handles closures internally is quite elegant. Most importantly for an embedding language, it’s got a really minimal build system that makes it really easy to integrate and its C API is quite nice. But the language has a lot of footguns, some of which can be avoided with better surface syntax. I usually reach for fennel.
You might be interested in Janet. The author is still Calvin Rose (the guy who wrote fennel), the VM and runtime is reminiscent of Lua’s (I find the C API to be much better, and the fiber concept is an interesting alternative to lua’s coroutine implementation), while being much less footgun-y as a language.
I’m curious, what did you find to be better about the C API? In my experience it was a lot more difficult to drive Janet from C than driving Lua. It did seem to handle the other direction pretty well though.
My use-cases tend to be the other direction, indeed: I wrote Jurl and janet-date (http client, I’m actually quite proud of the high level API design, even if the internal implementation could use some work, and proper datetime support).
Lua’s stack juggling when writing modules, lack of strong support for documentation and defining sources, and how you have to coerce the types to do what you want isn’t great. Janet’s abstract types are a really great way to encapsulate C objects in a way that feels native (in Janet), and the small but capable internals library means that you get to skip some of the hardest work in writing bindings (argument validation).
Anyway yeah, I admit I haven’t actually embedded Janet all that often (most cases thereof for me are jpm-built CLIs, so Janet is still the driver in a lot of ways) so I don’t have strong opinions in that direction.
This looks really good!
To each their own, I happen to really love it. Not gonna try to convince you otherwise, but would love to know what scripting/embeddable language you prefer. Not picking a fight or flamewar, just curious. Besides Lua, I enjoy Janet a lot.
Common Lisp is an excellent language, and is easily embeddable using the ECL implementation:
https://ecl.common-lisp.dev/main.html
Lua is is not perfect but it’s not terrible either.
The fact that a neat little Lisp like Fennel maps so nicely onto Lua precludes it from being terrible. It’s also too small to be terrible. You need a larger language surface to have a chance at being truly terrible.
I’ve never been drawn to Lua, but I really like Go–in particular, it’s minimalism and simplicity makes it really easy to get stuff done. If Lua is Go-like, I suppose I should give it a second look.
For what its worth I enjoy both Go and Lua and for similar reasons: both are languages that fit in my head, not too much syntax or magic, the code “does what it says” for the most part. Easy to read other people’s code.
Yeah… Another big wart: variables being global by default. The local variable attribute (
local x <const>) in Lua 5.4 is kinda strange.Using doubles as ints, then introducing 64-bit integer types in Lua 5.3 seems to introduce a lot of optimization hurdles. It’s nor favored by the LuaJIT creator.
just curious why do you think so? your github is full of Go projects, wat?
You can have projects written in language you don’t like? Like it’s more believable when someone has maintained many Go projects than not, if it comes to opinion if language is good.
Yes definitely. I’m very qualified to say Go is bad.
Care to expand your “a terrible language on top” ? I’m really interested in your opinion.
[Comment removed by author]
Re indexing for arrays, the author says “In Lua, indexing generally starts at index 1, but is only a convention. Arrays can be indexed by 0, negative numbers, or any other value (anything but nil).” I agree and disagree with “only a convention.”
Yes, it’s only a convention in the sense that a Lua interpreter will not throw an error if you create an array-like table starting from 0 (or some other number). But, no, it’s not only a convention in the sense that the standard library for tables and built-ins like
ipairsassume array-like tables with indexes starting at 1. So, for nearly all practical purposes, you probably want to index your array-like tables starting at 1. (As Programming in Lua says, “The Lua libraries adhere to th[e] convention [that array-like tables start at 1]; so, if your arrays also start with 1, you will be able to use their functions directly.”)A similar point applies to what the author says about nil-terminated arrays. If you know (or suspect or worry) that you may have a sparse array, you must not use
ipairsto loop over that table. Instead, you can usepairs(ornext) to get at the whole set of items without stopping at the firstnil. (If the order matters, however, you’d have to save the items in a new table and sort that table before use.)Maybe the bigger point is that Lua does not really have arrays in the sense of sequences. There’s just tables, and the tables are always key-value hashes. Sometimes the keys are integers, but—by a (strong!) convention—the indexing of array-like tables should start at 1 and usually the sequence shouldn’t have any gaps.
I think this is a pretty good way to put it. Another way to say it is that an array is not a data type; it’s a way of treating a table.
You can take any table whether it’s an array or not, and do array things to it. This will lead to surprising results IF you assume that
nilin Lua behaves likenilin other languages; it takes some time to internalize Lua’s meaning ofnilas the genuine absence of value. But once you do, it’s a good deal more consistent than how many other languages treat nil.This is true of almost every feature of Lua that people complain about; it’s not that it’s bad, but it’s frustrating if you come to it assuming it works like other languages.
This week I’ve been playing with Love2D, a simple game engine, wrote a prototype in Lua, and found it a refreshingly clear and a pleasant language.
Btw, the game Balatro - which is amazing IMO - is written in Lua on Love2D.
Balatro is awesome.
[Comment removed by author]
Have you seen the Playdate console? Its SDK is Lua based and should feel familiar to Love.
I’ve seen photos of it, but never bothered to learn more. Thanks for mentioning it, it looks pretty cool! Surprised to see it was made by Panic, who are (at least to me) known for macOS&iOS dev-specific apps and utilities.
UPD: Oh, apparently Panic is also a video game publisher! One of the games I really enjoyed was Firewatch, which TIL was published by them. Untitled Goose Game is also fun.
What’s the error handling like, after having tried it out for a while?
I notice that the examples just loads from file like this:
whale = love.graphics.newImage("whale.png")and:
sound = love.audio.newSource("music.ogg", "stream").LÖVE expects these files exists and panics (calls
error) if they don’t.This is fine because in a game, you should be able to expect all the required files exist. If you still want to handle the error, you can use
pcallas shown in the blog post.Another thing that I love about Lua is its coroutine implementation. It’s a very elegant implementation that composes different coroutines well. You don’t need to remember to yield to an inner coroutine like in python, it just nests properly and yields as needed. Return values also work like you’d expect. E.g.
This stuff is possible in other languages, but it feels more bolted on and more kludgy. With Lua, you write the straightforward cooperative threading logic, and it works as expected. I think that’s great :)
Maybe I’m too used to other languages, but that mostly works the opposite of how I’d expect! The idea that any function can call yield, and if it happens to be called by a coroutine it’ll yield feels pretty wild. What happens if you call
goutside of a coroutine? And is there a way to delegate to another coroutine?This feels like really dynamic effects, which is clever, but I wouldn’t want to have to be in charge of keeping track of.
It really opens up a lot of possibilities that you don’t have in languages with stackless coroutines, because it means that you can turn any code into non-blocking code even if it wasn’t written with non-blocking behavior in mind.
There’s a great write-up of how itch.io (the games site) uses this functionality for their DB access: https://leafo.net/posts/itchio-and-coroutines.html
I’ve used it before to integrate a REPL into a GUI where the REPL was not designed to yield, but coroutines made it really easy to integrate into the GUI’s event loop anyway: https://wiki.fennel-lang.org/Repl Never could have done that with stackless coroutines!
You get an exception/stack trace. I’m unsure (just from lack of trying) if any static analysis tools or LSP implementations can catch stuff like this, or if you just have to let it blow up at runtime like in my REPL example here.
Lua has 1-based indexing for strings as well as arrays. In my experience it’s a pain in the arse.
The length of arrays is tricky. It’s possible for
ipairs(t)and#tto disagree.I’m not a fan of languages that can’t detect misspelled variables at parse time. Perl was my first dynamic language, and I was dismayed that other dynamic languages are worse at detecting typos.
Lua’s 1-based indexing hasn’t bothered me, and I’m saying this as a long time C programmer (35 years, and before that, assembly programmer), but that could be just me. In fact, I recall having a harder time going from assembly to C as there were things I could do easily in assembly that were hard (at the time) in C (my biggest complaint at the time—the inability to return multiple values easily).
I wouldn’t really say that they disagree, so much as that they both can work in ways that are surprising to people coming from other languages or with specific expectations. But maybe I’m misunderstanding what you mean. Can you give an example of the disagreement you have in mind?
ipairs()is an iterator, so it’s use it like:It works by starting at 1, and continuing until an index returns
nil.#is the length operator, and for tables, it does a binary search to find the ending index (seriously). So as an example, if you had the array{ true , true , nil , true , true , true , true , true }(element at index[3]isnil) then it could return 8, or maybe 2, depending on how the binary search jumps around.Arrays in Lua have always worked this way (as far as I can tell). The terminology around this is a bit strange, but Lua calls what we would call an “array” a “sequence”—a table has elements at array indexes starting at 1 and each subsequent index increases by a whole number and ends with an index returns
nil(marking no entry; as an optimization, it will keep such a sequence in an actual array, but it has to be constructed pretty much in order for that to happen). The length of such a sequence is, as stated, found by a binary search.One consequence of this is the inability to determine the difference between “undefined” and “no value”. In other words, every table has a “value” for every key you could imagine; it’s just that most of the values are
nil. Anilin a sequence:{ 'a' , 'b' , nil , 'd' , 'e' }is undefined by the Lua language semantics. Enough people complained about this that the team addedfalseto address this issue (and addedtrueto have an actual Boolean value). The only two values in Lua that are treated as false in a Boolean sense arenilandfalse(such thatif 0is considered true because the value exists). If you want anil(logically, not physically) in a sequence, you are supposed to usefalseinstead, as that will keep the sequence legal in the language sense.{ 'a' , 'b' , false , 'd' , 'e'}is a valid sequence of length 5. Before using such a value, you useif value.Edited to add: the description of how it works in the Lua manual.
As the documentation that I linked to says,
#tfinds an arbitrary border in the array part of the table, and as the article says,ipairs(t)finds the first.Got it. I understand what you and spc are saying, but I don’t see this as a disagreement. As I understand the documentation, neither
ipairsnor#offers an answer to the question “What is the length of a table withnils in it?” That doesn’t seem unreasonable to me since the question does not have a single, unambiguous answer. In any case, thanks for clarifying.It’s not that it’s an ambiguous question; it’s more like an impossible question. Asking for the length of a table with nils in it is like asking how many nothings you can fit in a bag before it gets full; the question itself is kind of nonsensical given Lua’s definition of nil and table.
Honestly this used to be a much bigger problem before LSP became a thing; nowadays it’s very easy to detect mistyped identifiers as you type. (Even without LSP it’s easy to catch with
luacheckbut before LSP that required a separate manual step.)Is it though? I feel like I hear about it all the time. According to various surveys it’s one of the most popular languages in the world:
https://survey.stackoverflow.co/2023/#section-most-popular-technologies-programming-scripting-and-markup-languages
https://www.techrepublic.com/article/lua-moves-into-top-20-of-marchs-tiobe-index-python-retains-top-spot/
https://www.developernation.net/blog/language-communities-who-leads-the-way/
I”ve been using Lua quite a bit recently. Sol3 makes it incredibly easy to embed from C++, you can call cast Lua functions to
std::function, expose functions to Lua by just assigning them to indexed elements in the Lua VM wrapper, and expose entire class types by just passing method names and pointers (or lambdas if you want the Lua version to be a lightly sugared version of the underlying C++ one). It transparently converts Lua tables to C++ collection types (anything with iterators). Unique and shared pointers inter operate with Lua memory management, with the destructors being called from the GC.100% agree. I embedded Lua into my hobby 2D game engine and with macros and a bit of template magic to unroll and pass function parameters and return values as appropriate types it’s absolutely magical to be able to bind things from C++ with only a few lines.
Not mentioned in the article is the amazing hooks Lua provides to be able to examine the state call stack, profile, and other options. I’m able to generate incredible script error reports with not much more than the built-in capabilities.
I posted in another thread, but if you haven’t already used it I’d recommend taking a look at Sol3 it provides a lot of the template magic to do very nice Lua bindings from C++. It also abstracts over various Lua implementations so it’s trivial to swap them without needing to modify your binding code.
I love Lua. My favorite thing is that being a small language, I don’t spend a lot of time thinking about the right way to do something. If I can bang together a loop over a table to do my computation that’s good enough.
One interesting Lua application out there is Path of Building, a sort of theorycrafting simulator for the game Path of Exile. It’s a giant complex pile of Lua code with many outside contributions. And has an intensive GUI too. It works just fine.
This post comes down to: lua is fast and embeddable, and two notes about arrays (which as @telemachus says may not be correct).
I’ve been enjoying lua-for-configuration in Neovim and Wezterm recently while setting up a new computer. Especially in Neovim, it’s so great to go from the very quirky and weird Vimscript config to Lua where a setting looks like a setting, and a function call looks like a function call. More or less.
Neovim and many modern written-for-nvim plugins also sport great type documentation coverage, so you can get great completion and in-editor type checking… once you get your config running and install the lua checker.
For general programming I find the situation with array/list/sequence “nil termination” unacceptable. It’s bad enough in C strings but to have the same kind of wonky issue for the basic “ordered compound type” in a dynamic language is just… ugh. It will hide nil pointer mistakes and instead turn them into silent data loss, horrendous. I guess people deal with it because large amounts of production lua do exist, and there is the type checker stuff. I just don’t want to have to think about such things when programming.
I have used both and i cant say i enjoy using lua at all. Instead, i would prefer something simpler, preferably with typing support for better ide/lsp support. In my experience, buck2 starlark got it just right.
My favorite part of Lua is that it has so many compile-to-Lua options (similar to JavaScript) which means you can choose something you personally find more ergonomic.
Lua is a language I’d recommend anyone check out, and I’m glad to see an article showcasing some of its coolness. That said, I don’t know if I’d recommend it for anyone’s primary programming language. (Not to say that it shouldn’t be!) I’ve used it for configuring WezTerm, as well as some fun coding adventures in Minecraft through ComputerCraft: Tweaked and similar mods. At one point, I even was working on a Twitch chat display for fun.
It’s got a lot of power, and its proliferation definitely comes from its ease of embedding. Though it’s a language that, to me, always feels foreign. Coming from mostly JS/TS, and everything feels just a little “off” when working with it, but the language is one that’s probably easiest for me to hold in my mind while I’m using it. And there are some projects like Moonscript that transpile to Lua that can make it feel a bit more familiar.
I’m hoping to build up enough interest in a game development class to pilot a course using Lua. We shall see. If it happens though there are a lot of opportunities for learning that I’m really excited about.
[Comment removed by author]
Tables as the only container type in Lua is something really interesting to me. It’s like the cons pair in Lisp. There’s some tantric superpower in the simplicity of a network of tables that enlace.
It’s not that different from Javascript, it’s just that the Lua authors have avoided and removed complications, whereas Javascript has added them.
I really feel like JS and PHP have caused people to view “one data structure can act as a key/value dictionary as well as a linear sequence” with suspicion, but yes, Lua shows that it’s possible to do well.
Oh, and Lua avoided the mistake of stringifying keys.
The only two values that can’t be keys in a table are
niland NaN. Every other value can be used as a key (even a table).Idk making nil an illegal array element is worse than any of JS’s goofs imo
It’s just the way to remove elements from a table – you set them to nil, the default value.
You can have arrays with holes in Lua, but then the value of
#would not be reliable.I think the way it works makes sense given the rest of Lua’s design, doesn’t it?
Yes. It took a long time for me to get used to it, but at this point it feels so clunky when I go back.
Most languages just have so much unnecessary confusion over “does this hashmap actually have nil at this key, or is it absent altogether?” which is just … an entire class of problems that does not need to exist.
Stop using bad languages :p
Treating your “no value” value as actually no value consistently is better than being inconsistent with it, but it’s worse than simply not having such a thing in the first place.
print(foo)whenfoohasn’t been assigned to should be illegal, not printnil.Yeah, so… I’m literally the lead developer of a compiles-to-Lua language that makes this a compile error like you say. But that is completely separate from how nil behaves inside data structures.
Overall I’m not a fan of nil, and I would prefer the approach of a language like Erlang which omits it entirely over the way (for example) Clojure or Ruby handles nil. But I have a hard time seeing how that could be viable in Lua without compromising on the ruthless simplicity that is Lua’s primary design goal.
equality on an table is guaranteed to be uniquely defined for that table only. it’s not a particularly hard problem, and I’m sure you know this too. You get the choice to do it that way if you want, it’s just not built into the language.
Lua, like C, is pretty minimal at its core, but all of the functionality is there for you to implement if you want. I think the thing people don’t like about Lua is that they have to program and make decisions about things and there’s not a batteries-included-opinionated-make-the-decision-for-you default, which I personally don’t mind.
I have my own Lua and my own style.
Also a fun thing I discovered the other day, since I remember you from my neovim days, and you might appreciate it.
It’s funny how you can’t do
"%q":formatwithout doing("%q"):format, but if you just prefix it with a function call, the parsing works out fine :PBut real Lisp code uses actual arrays, not linked lists.
Worked on an embedded lua project. The C API is a pleasure to use.
And even better, the type system lines up so well with a certain other language that you can use TypeScript to write it: https://typescripttolua.github.io/
Except that Lua’s type system does not really line up with that of JavaScript, and TSTL is very much aware of this.
https://typescripttolua.github.io/docs/caveats#differences-from-javascript
So it feels like you’d end up writing TypeScript that is so dependent on Lua quirks that you might as well work with the language and write Lua instead.
I don’t mean the nitty-gritty of the type system like == equality and null vs undefined etc., but rather that the primitives are mostly the same (Lua even treats numbers as floats, like JS!), and you make compound types by using objects/dicts/tables, and arrays are just a special kind of those.
They even say:
I used it for an ad-hoc NeoVim extension once and found it worked pretty well.