I use Lua a fair bit in my day job. The comment about it being a minimalist toolkit that allows you add scripting on top of domain specific tools is spot on. That is exactly the use case that Lua shines in.
My favorite feature is the way that tables and closures allow you build up just as much of OOP as you need for the application. It feels very empowering. I'm working mostly solo, so I've been able to establish my own conventions and stick to them. Doing this as a large team would be fraught with peril and require strict standard practices to maintain an intelligible code base.
I was surprised the author didn't mention my #1 complaint, variables default to global scope AND initialized. If you misspell a variable name Lua will happily initialize it to nil and run with it. This almost always results in subtle incorrect behavior instead of an error.
Amusingly, in a sense, your complaint about global variables is because of what you like about how generally simple Lua is plus how powerful its tables can be.
i.e. The global scope is a table, and variables are keys on that table, so of course the value of any variable is `nil` because that's how tables work and Lua isn't going to special case a table just because it's a magic part of the environment.
...but, naturally, since the global environment is a table, you can set a metatable on it and require that variables be declared before you use them. :D
To be clear, I'd like all tables to throw an error on unresolved key names. And I can't think of any cases where the current behavior is necessary for any of the features I like.
I think the design decision to have unresolved names result in defining a new variable, initialized to nil, is the result of a different design goal. I'm not sure what that goal was, something about fault tolerance maybe. In practice I find this feature means I need very thorough test coverage, or a metatable, as you've noted, or a static analyzer, as others have noted. Or a combination of all the above.
unresolved names result in defining a new variable, initialized to nil
This is not what Lua does. All table lookups return a value corresponding to a specified key, if there is a correspondence. Or simply nil, doing nothing in this case. There are no global variables per se, only a table that non-local lookups end up into.
In practice I find this feature means I need very thorough test coverage, or a metatable, as you've noted, or a static analyzer, as others have noted. Or a combination of all the above.
You may choose a language that fits your needs rather than trying to use something that doesn’t. E.g. I think Python more or less matches your expectations here. Lua doesn’t have to please everyone, and trying to make X out of it makes no sense neither for you nor for Lua designers, if X already exists.
That said, I see this as an overdramatization. Strict metatable on globals solves the issue with “automatic global variables”. But other things like static analysis and thorough testing will be required (and require your work too) in any language to achieve the “high-horse” level of guarantees.
Matter of taste, then, I think. Or of how you're thinking about the language, maybe?
It's not really that they're "defining a new variable" -- absent metatables, all variables always exist, and there's not a difference between one that you've accessed and one that you haven't. Behind the scenes there might be a memory allocation difference between a variable that's never been touched and one that's been used and then set back to nil, but to the user that shouldn't really matter.
I'm guessing here, but the goal is probably one of simplicity -- this means there doesn't need to be special language features around declaring variables that users need to be aware of. It ties into the article's discussion of 1-indexed arrays, as things that make sense to humans who haven't done weird things to their brains as we programmers have.
(Granted, `local` is somewhat a language feature for declaring variables, so there's some compromise here.)
I'd probably align it with OOP in the Lua discourse. It's something that the language doesn't provide by default because it's simpler not to... but if you want it, the language makes it pretty easy to add through metatables.
Also, do not forget an easy and powerful sandboxing that you can achieve with Lua by selectively including/excluding parts of its std library at compile time and running Lua itself in a thread with no (or minimal) shared state. This alone is priceless when embedding scripting into an application.
Regarding globals, defaulting to local scope is a thorny design choice. Try this in python:
def make_counter():
def incr(x):
total = total + x
return total
total = 0
return incr
I think Lua would be a better language if they were to eliminate global variables entirely, and flag as a load-time error any undeclared variable that is not present in the environment supplied to `load` or `loadfile`.
Setting __newindex on the global table to throw errors is a not-too-bad solution, but these will be run-time errors, so to catch them all you need good code coverage (yes, you want that anway) and uncouth third-party libraries can trigger those errors.
EDIT: re-reading your comment I think I see now you were agreeing with me. :-) Leaving this here as elucidation.
That python code throws UnboundLocalError, which seems like what it should do to me. If you move total = 0 to before def incr(x): then incr closes over the local total variable. Every call to make_counter returns a new counter with its own state.
The equivalent Lua defines total as a global (whether total = 0 is above or below). Multiple instances of incr are all changing the same global variable. That doesn't like the intended behavior.
The existence of `nonlocal` is an indictment of the local-by-default design.
(Parenthetically, for completeness, I should note that this is a problem only when the language supports mutable variables or, more precisely, mutable captures or "upvalues", which I think are also a bad idea, but that's another bag of worms for another day.)
Python would be a better language if it had Lua's local. Because Python would have true lexically scoped variables that way, instead of the current system.
> If you misspell a variable name Lua will happily initialize it to nil and run with it. This almost always results in subtle incorrect behavior instead of an error.
If your code editor doesn't warn you that you're potentially reading a variable before writing to it, then you need a new code editor.
Originally it was a configuration language. That global syntax makes it friendly to people writing configuration files. Anyway, if that bothers you, you might want to use luacheck or some other linter which will pick it up.
> my #1 complaint, variables default to global scope AND initialized.
Lua, originally, was a configuration language. You know, the sort of use-case you use a .ini (or these days, YAML) file for. Except you have realized you need some logic to set your flags, so you decide to make your config file programmable.
It has evolved to its current form but these roots remain. They should have added a 'strict' option.
My favorite feature is the way that tables and closures allow you build up just as much of OOP as you need for the application. It feels very empowering. I'm working mostly solo, so I've been able to establish my own conventions and stick to them. Doing this as a large team would be fraught with peril and require strict standard practices to maintain an intelligible code base.
I was surprised the author didn't mention my #1 complaint, variables default to global scope AND initialized. If you misspell a variable name Lua will happily initialize it to nil and run with it. This almost always results in subtle incorrect behavior instead of an error.