Hacker News new | ask | show | jobs
by nequals30 749 days ago
I'm wrapping up a multi-year personal project, a game written fully in lua using love2d.

To me, the beauty of lua is the simplicity and lack of learning curve: I can usually accomplish whatever I need to without looking anything up (as the author said, everything is a table so there isn't much to overthink). Also, the community and support around love2d is fantastic.

One thing that's bothered me is that lua silently returns nil when you reference non-existing elements. That's been a pain when debugging, since a line with a typo (`a = typo`) doesn't fail, and the code fails much farther downstream (e.g. when doing arithmetic on `a`). So almost all my errors end up being "trying to do operation on a nil value", and there is no indication of why it's nil.

8 comments

I think you can add a metatable to _G with an __index function. This should be called when accessing undefined local variables (as they end up trying to access the global scope _G) and you can thrown an error.
You can. In our game engine ~10 years ago we would hook the global table to stop designers from creating globals (the default in Lua without using the local keyword) at certain areas in the game frame, mostly to stop this exact trap.

I toyed with the idea of inverting the semantics of global and local in lua and to remove "local" and instead default to local and have a "global" keyword. Looking at lua.c quickly dissuaded me when I was a much more junior programmer, but now days it might be fun to try.

Local by default would have been a much better choice. The entirety of my code looks like local this local that. In all my years of Lua I have only found 1 reason to use a global variable: in Love2D when loading a texture, a local variable will go out of scope and get garbage collected, resulting in a black texture, but a global variable will not. Maybe it's even been fixed by now, as that was years ago.
Haven’t done enough Lua to know if this possible, but is that monkey patchable?
Yes, you can control a namespace (in the form of a table, of course) in which a module you require executes. The global definitions are then placed in the table. You can implement copy-on-write using this namespace and a metatable, you can change the semantics of accessing an uninitialized (global) variable, etc. Lua is incredibly flexible, after all. Unfortunately, since you have to implement (and then maintain!) those yourself, it's hard to justify (IME) using Lua instead of a more full-featured solution (that might still incorporate Lua somewhere in the stack[0]), esp. since the main selling point of Lua is simplicity.

[0] You can use something like Fennel or Haxe to compile a more structured language to Lua, or you can use an alternative implementation like Luau.

A linter like luacheck is better for accidental globals, but I think the GP was talking about doing a = someTable.typoedFieldName.
My biggest gripe with lua was that depending upon the internals of the implementation, it could "swallow" an error entirely. The program would just die in absolute silence and not give an error at all or any indication it was still running.
> One thing that's bothered me is that lua silently returns nil when you reference non-existing elements. That's been a pain when debugging, since a line with a typo (`a = typo`) doesn't fail, and the code fails much farther downstream (e.g. when doing arithmetic on `a`). So almost all my errors end up being "trying to do operation on a nil value", and there is no indication of why it's nil.

I am also making small games with love2d. I've found you can prevent many of such issues if you:

1. Create objects with private fields, using getters and setters to access values (as function calls will crash if you call them and the functions don't exist, unlike fields). I like an approach that is somewhat similar to this: https://www.lua.org/pil/16.4.html

2. Add assertions liberally, especially in constructors.

Use LuaCheck if you don't want to use the LSP. It warns about the use of globals, which is generally what you want. It's perfectly possible to design your program to use no globals.
If you use the lua lsp, you can make type annotations which basically work like jsdoc. With those annotations, the lsp will warn you about such issues, there is a diagnostic that's called something like `needs-nil-check`.
I really really don't want to do type annotations. I just want to know about:

  variable = 1
  print(varaible)
It's been a while since I did Lua, but there's been a few cases where this caused massive confusion.

Last I checked I couldn't really find a good way to do that; but like I said: it's been a while.

Luacheck will warn about unknown globals.
I think every lua linter warns about that. So does the Lua LSP, no annotations needed.
Well, yeah, you shouldn't name your variables so similarly, that's what's causing he confusion. Idunno, maybe varaible is set to "Let's groove, baby!" ? ;-)
That has been nice (at least for editing neovim configuration files). But what if I am editing anything else?

For example, I have then tried to edit Wezterm config files and there are no types. I did find some types someone made online but no idea how to instruct my editor/lsp where these types are or what they are for.

Yeah, it's only for nvim or for your own lua projects.

For WezTerm annotations, afaik there is currently only an open issue without much progress: https://github.com/wez/wezterm/issues/3132

You can define a metatable on your objects of interest (or the root table meta table if you don't mind breaking the language's conventions and thus libraries) with __index and __newindex members. Then you can throw in those by calling the `error` function when they'd otherwise normally return nil, should you desire it.

But runtime checks have a cost, and static types that transpile away are a bit better for overhead so long as you don't mind the build step, so using one of the typed lua variants is probably a bit nicer in the long term. Catching those typos early is their bread and butter.

What do you want it to return though?

x = f() or "default value"

or maybe you want it to error

x = f() or error"F failed"

or maybe x is an object in which case you can just

setmetatable(x,{__index = function () return "" --[[default value here]] end})

or maybe you don't want nils, in which case

debug.setmetatable(nil, {}) -- actually this one is complicated, but yeah, you can use this.

The default should be an error, since that's the only way to prevent issues where the wrong value gets passed around a lot before finally blowing up somewhere else (and good luck debugging that).

In cases where you really want to fetch the value or else get some default if it doesn't exist, there should be a way to do so that is distinct from regular dereferences and element access.

I do not know how/why the language should magically know that something should error, if nil is not acceptable as a return then you use || or "this". if the table should not have nils then you turn it into a metatable that errors or returns whatever you want it to.
The language should not "magically know" anything. It should do the safest thing, which is not to silently return a marker value for which there is no guarantee whatsoever that the caller will remember to check it. If the caller does not want to see an error, then they should use a different method of retrieving the item that is specifically defined as returning a marker value. E.g. in Python:

   d["foo"]          # exception if missing
   d.get("foo")      # None if missing
   d.get("foo", 42)  # 42 if missing
This follows the principle of least surprise - [] is the standard syntax for indexing, so it raises exceptions, and if you forget to check you get an error right there and then, not an unexpected but valid value (that might be saved into a variable etc and then break things much much later!). If you need a market value than you must use get(), and the very act of doing so indicates the intent to both the language and to another person reading your code.
This is your opinion on what the default behavior of nil should be, fortunately, We can disagree and you can set a metatable that errors on nil, I will choose to do or not do that depending on the object.

assert() also exists for this very reason.

PS: there is a performance penalty between doing d["foo"] v d.foo, one expects an expression, the other does not. So it cannot be compiled. lua also errors on use of a nil value, d.foo() will error, d.foo + 3 will also error. So while it does "silently return a marker" it will crash on runtime.

I have not looked at Lua but this:

> I can usually accomplish whatever I need to without looking anything up"

And "everything is a table"

Makes it sound a lot like SQL.

Edit: I prefer replies to downvotes.

Lua isn't anything like SQL. "Table"s in Lua are like maps/dictionaries/hash tables, not relational tables.
But still maybe the concepts for working with them (group by/ join/ aggregate) are similar?

There are lots of things you can do with a table as a general purpose data structure. And I was curious if this was a similarity?

When you say that tables are like dicts/maps, are they tables or are they dicts?

And I apologise for not knowing more about Lua. Maybe i should have known better than to ask about things I dont know.

> Maybe i should have known better than to ask about things I dont know.

It is fine to not know things. But when I look at your post, I see that you didn't ask any question. You only made statements that were based on very unfounded assumptions.

But to get back to what tables are: They are key-value pairs with both array- and hashmap-semantics. By convention, arrays start at 1 in Lua. They are very similar to dictionaries in Python , associative arrays in JavaScript and other key-value data structures in other programming languages. However, lua also uses them for storing variables in environments (some people would call environment "scopes") and there are special callbacks for missing variables and accessor functions when working with these tables.

> associative arrays in JavaScript

You're thinking PHP. Arrays and objects are discrete things in JavaScript. You can add random properties to arrays (since they are also objects) but don't expect them to behave well when doing things line loops, getting they length, etc.

JS objects are associative arrays with some fluff on top.
Yeah I forgot a question mark.
So it seems like what they call tables are in fact maps or dicts. "Symbol tables".

I initially raised my eyebrows since what actual tables would have brought would be joins on non unique keys and multiple columns.

That would have unlocked more versatile "table programming" as in SQL, pandas, R and to some degree Excel.

The real benefit of "table programming" is that it is default distributable. All operations are atomic and immutable.

(I also lost some interest in Lua after -4 downvotes).

https://www.lua.org/pil/2.5.html

“The table type implements associative arrays.”

Simplified (and some hand waving): In Python associative arrays are called dict, in Lua they are called table.

It’s really much more like JavaScript (due to its prototypical nature), where “everything is an object” is effectively saying the same thing.
It's much more like JS without all the prototype stuff.
Aren't Lua metatables and JS prototypes almost the same mechanism?

I say almost, because I think metatables are more broad than JS prototypes - you can emulate JS prototypes behavior using Lua metatables[0], but I don't think the reverse can be done (maybe with some Proxy[1] hackery?).

[0] https://www.lua.org/pil/13.4.1.html

[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

Yes, you're right, `__index` does behave a lot like `__proto__`. I often forget that it is one of the few entries in the metatable that doesn't have to be a function.