Lua: Good, bad, and ugly parts
notebook.kulchenko.com
notebook.kulchenko.com
One of the goals of this project was to compile effects code to JavaScript with Lua.js[3] and produce a demo that ran in the browser. There, unfortunately, I ran into a showstopper with a Lua.js bug[4] that breaks my approach to creating modules. Unlike LuaJIT and regular Lua, Lua.js is an experimental project and far less mature - though totally awesome. I might make another attempt to fix the problem myself at some point. With a more mature Lua.js, you could write fast Lua code and port it to nearly every environment.
[1] https://github.com/graue/luasynth
[2] https://github.com/graue/synth
[3] https://github.com/mherkender/lua.js
[4] https://github.com/mherkender/lua.js/issues/13#issuecomment-...
I chose lua because of its built-in coroutines. For a simple mud server, blocking until the next line of input is ready works really well. Because it's not preemptive, you don't have to worry about locking. Because MUDs are so niche these days, you don't have to worry about the userbase scaling out of control.
Never did get around to writing a mudlib, though...
https://scott.mn/projects/luasynth.html#filter_effect
In short, I ran C and Lua implementations of the same command, 6 times each, using the built-in "time" command in bash. Averaged the "user" time for each implementation and compared the results. As input, I used a 2m:38.040s long song, 44.1 KHz stereo, converted to a raw file with 32-bit float samples, and I redirected output to /dev/null.
For the first benchmark[1], which was faster for LuaJIT, I actually messed up: I compared the "real" time rather than the user CPU time. But as I recall, the "user" times bore the same relationship, and there was very little deviation among the six trials. If you don't trust me, feel free to do a more rigorous test!
It's worth mentioning that the effect for which LuaJIT beat C was a simple gain, so what was really being tested was a loop that multiplied a bunch of numbers by a (within the loop) constant. The second test in which Lua was a factor of two slower may be more representative.
To build the code, clone the graue/synth and graue/luasynth repos on GitHub. synth includes build instructions, while Luasynth merely requires you to install LuaJIT and run the script.
It never ceases to amaze me, as a language and as an interpreter. Sure, the syntax bites you occasionally, and sure, the lack of built-in functionality is occasionally annoying. But hey, I can add callbacks into a simulation engine that execute 10 million times per second with almost no slowdown in the result. Like the article mentions, the string matching library isn't as powerful as Perl's, but it's more than adequate for all of my uses.
My only wish is for an updated LNUM patch that works with Lua 5.2. I deal with integers too much in my projects for its absence to not be annoying.
[0]: http://luajit.org/ext_ffi.html [1]: http://andrew-d.github.com/lua-ext/
But all in all it's surprisingly effective.
local array = { one, two, threee, four }
A gap is silently created in the array because Lua returns nil for undefined variables. This causes the (#) operator to an return incorrect result and can lead to subtle, difficult-to-find bugs in the program.Lua makes no distinction between nil and a non-existent element, so assigning nil to an array element will also create a gap, rather than shifting the keys of the subsequent elements downward:
local array = { 1, 2, 3, 4 }
array[2] = nil
-- array is now { [1] = 1, [3] = 3, [4] = 4 } local array = {1, 2, 3, 4}
table.remove(array, 2)
-- array is now { [1] = 1, [2] = 3, [3] = 4 }Your first example has a point – perhaps undefined variables should error when accessed – but that's got nothing to do with tables.
> Your first example has a point – perhaps undefined variables should error when accessed – but that's got nothing to do with tables.
Well, of course it does. The point is that you can silently end up with array "holes" due to a typo, which you are supposed to avoid in Lua for a number of reasons, such as an the length operator returning an incorrect result. I don't understand why my previous comment is getting voted down for pointing this out.
Object in Smaltalk.
(Tangentially, I don't agree that conflation of array and object is a good thing)
I've been teaching myself Lua recently and I've found it to be a nice, consistent, small language. Javascript is a fun language, but has some bad decisions built in: http://oreilly.com/javascript/excerpts/javascript-good-parts... and it can be really confusing at times: http://wtfjs.com/
The way things are going, it looks like that alternative will also take advantage of Lua's embeddability to allow interop between web apps and web servers in a way that hasn't been widely practiced before.
Do you know Luvit? http://luvit.io/
?q=javascript+wtf / ?q=javascript = 6.8M / 3.04G = 0.002233
?q=lua+wtf / ?q=lua = 844k / 109M = 0.008110
https://github.com/mherkender/lua.js
Unfortunately, there is a bug in Lua.js and it doesn't support the preferred method of creating modules[1]. For this reason, I've yet to get a substantial size app written. I'd still like to go back and fix this myself eventually, but I was sinking a lot of time into trying to get it to work.
[1] https://github.com/mherkender/lua.js/issues/13#issuecomment-...
I've been considering several different languages to start them on and Lua has turned out to be a great choice. My children both love minecraft and we've been writing programs in Lua for the computercraft mod: http://www.computercraft.info/
How is it in practice?
For loops are more natural when indexing from one; for loops in most languages specify inclusive ranges, but indexed containers typically advertise their, rather than their maximum index. You don't notice this much in C and derived languages because the for loop in those languages is more flexible (and more open to abuse).
More concretely, this is a very common idiom in Delphi when dealing with 0-based containers:
for i := 0 to list.Count - 1 do
The '- 1' on the end is ugly. What's needed is a way of expressing a half-closed interval, but that's not common in programming languages (despite it being a great aid to correctly expressing many algorithms concisely).Let's say you want to insert into the middle of an array, at x, with count elements (and presumed free space at the end). You need to move all the elements from x to the end up one. Let's suppose you have a move(array, fromIndex, toIndex, count) function that can do that. In 0-based languages:
move(array, x, x + 1, count - x)
But you need to adjust it for 1-based languages: move(array, x, x + 1, count - x + 1)
However, you can mitigate it by changing how the move() function takes its parameters: move(array, startIndex, endIndex, newIndex)
move(array, x, count, x + 1)
If everyone is consistent, and APIs are designed with 1-based indexing in mind, most of the issues go away. It's when there are mixed styles that things get painful.A slightly lesser known construction in Ruby, the ... Range operator does this.
It won't be fixed, because it would break existing programs in too subtle ways. Lua compatiility is sometimes broken, but only if it breaks into easy-to-find-and-fix ways.
Using a single dimensional array as a 2 dimensional array relies on zero indexing.
index = y * width + x
array[index]
Using modulo to covert an index back to 2 dimensions relies on 0 indexing x = index % width
y = index / width
Skipping a prefix in a string. prefix = "ABC"
string = "ABCdef"
unprefixed = string[prefix.length:]
I'm sure there are tons of others. Yes, you can add 1 or subtract 1 in the right places. That's the annoying part of it though.For example, to get a prefix of length 'n', you need characters 0 through n-1 with zero-based indexing and 1 through n with one-based indexing. (That Python hides the -1 when using a [0:n] range is a convenience; the subtraction still happens internally.)
Similarly, to get the last element of a one-based array with a[length(a)] and the last element of a zero-based array with a[length(a)-1].
What it comes down to is that sometimes you have to write +1 or -1 for zero-based indexing where you can avoid it for one-based indexing and vice versa. What is not the case is that it only universally happens for one of those schemes.
More elaborate and rigorous: http://www.cs.utexas.edu/~EWD/transcriptions/EWD08xx/EWD831....
Whether it's a very mildly annoying annoyance or it's the most annoying thing in the world seems to depend entirely on your personality.
arr[0] = n
and write your own for loops, e.g. for i=0,10 do print arr[i] end
The only catch being that the length operator (#arr) will be off by one. for i, x in ipairs(mylist) do
so the 0 vs 1 based indexing is made irrelevant. There are some times where mess up but overall most Lua users don't think its much of an issue. (Personally, I find the lack of a ternary conditional operator much more annoying)http://lua.2524044.n2.nabble.com/petition-to-keep-ipairs-ali...
Edit: both are from 2010. This never became final perhaps?
http://computerscomputing.wordpress.com/2013/02/18/lua-and-s...
The best reason against using Lua in my mind is that undefined variables in Lua return nil, which can lead to typo bugs and therefore unintentional sparse arrays, which screws up the result returned by the length operator. It also has inconsistent boolean expression evaluation:
while 0 do print("Loops forever") end
while not 1 do print("Does nothing") end
while 1 do print("Loops forever") end
while not 0 do print("Does nothing") end
It's nice having real classes in Squirrel with type introspection and corresponding C APIs. In Lua, I have to juggle metatables in the registry to construct a class system, and my system will differ slightly from someone else's. Squirrel's other niceties like compile-time constants, 0-based arrays, and automatic reference counting for predictable memory management overhead make it a quite nice alternative to Lua.It sounds like I'm bashing Lua here, though I still find it fun to program occasionally. But I do wonder how long Lua can remain as popular as it is with an unconventional syntax, inconsistent behavior, and a minimal built-in library in the face of richer alternatives like Squirrel, mruby, and even Tcl, which has improved much in recent years. There must have been a reason Valve Software chose to use Squirrel in L4D2 and Portal 2 instead of using Lua as most apparently do.
I'd rather say Lua is one of the most consistent and logical languages when it comes to truth values. You don't ever have to worry about arbitrary value evaluating to false.
function Func1() return 1; end
function Func2() return true; end
if(Func1() == Func2()) then
print "They're both the same!"
else
print "They're different!"
end
In Lua, even though 1 and true evaluate to true, they're not the same thing and so comparisons between them evaluate to false!There's no way to avoid this problem in any language that allows coercing a number to a boolean. There are two possible boolean values, and many more possible numeric values. Therefore, numbers that are different and compare unequal will still both evaluate to true. Pigeonhole principle.
It seems you were surprised by the fact that Lua doesn't consider 0 false. Fair enough, that behavior's different from many other programming languages. But there's nothing inconsistent about it.
It's not a dealbreaker. It's just a Lua quirk that often surprises newcomers. I was pointing out that Squirrel uses more conventional boolean evaluation that script authors coming from other languages may be more comfortable with. If you're exposing a scripting API, the language you use is essentially a part of your user interface.
But I think I see what you are trying to say. You want the == operator to coerce its operands to the same type before comparing (like JavaScript's == operator), but Lua's == operator doesn't do that, it simply compares. And that's why other languages need an === operator and Lua does not.
(JavaScript, by the way is the inconsistent one in this regard: the == operator does coerce its operands, but if(something == true) doesn't do the same thing as if(something). Try it with an empty array)
$ irb
irb(main):001:0> if 0 then puts "0 is truthy!" end
0 is truthy!
=> nil
irb(main):002:0> if (not 0) then puts "(not 0) is truthy" end
=> nil
irb(main):003:0> if (0 == true) then puts "0 is equal to true" end
=> nil
Many things about many programming languages are surprising to newcomers. However, the rule is very simple (in Lua): In a boolean context, every value besides nil and false is treated as though it were true. It is absolutely consistent. (a == b) is not a boolean context, or else 2 == 3 would be true. if x then print "OK" end
prints "OK" but for which x == true
returns false. Is that right? If it is, then this is completely normal for languages that allow non-booleans in if statements. For example, x=2 is "inconsistent" in very many languages such as C, Python, Ruby, Perl, most Lisps, etc. I've never heard of this Squirrel language before, but if I read the docs correctly, x=2 is also "inconsistent" in Squirrel. In fact I'd be very surprised by a language that had no "inconsistent" values of x and that didn't achieve this by simply restricting x to be Boolean in an if. while 0 do print("loops forever") end
while not 0 do print("does nothing") endI do like Lua and - per the discussion below - I don't see any problem in Lua's concept of truthiness. It's different from other languages like C or JavaScript, but similar to others like Ruby. Just something you have to get used to if you program in multiple languages.
But the other things you mentioned do sound worthwhile, so thanks!
if 1 then print "true" end
if 1 == true then print "never prints" end
Not a reason to avoid the language. But it may be a reason to consider an alternative with more conventional behavior.The == operator does not compare the "truthiness" of two values. It compares the values, and if those values are two different types, they are always unequal.
> if 0 then print "true" end
true
> 0 == true
=> false
I know why this is the case. But it's a logical inconsistency for users of the language.While true, you should iterate integer-key tables using ipairs and hash tables using pairs.
> No continue statement
Continue doesn't exist? I swear I've used it in Lua 5.1.
> While true, you should iterate integer-key tables using ipairs and hash tables using pairs.
In my opinion, this makes Lua's combination of arrays and dictionaries pointless since one must distinguish between them anyway for iteration.
Either way you should be using not-pairs. If you construct a table in a weird way you can end up with pairs going out-of-order. ipairs works fine, also "for i=1,#t" works fine.