I don't feel like I have the greatest grasp on Lua metatables/metamethods, but I came across some unexpected behavior this week and was wondering if this is a bug. Example cart attached.
I'm setting up a "class" like so:
class = {} class.__index = class function class:new(instance) local instance = instance or {} setmetatable(instance, self) instance.__index = instance add(self, instance) return instance end |
To save tokens, and since I never expect to have numeric indexes in class
or any subclasses I instantiate from it, I simply add the instance to whatever object is self
when :new()
is called. ie, class[1]
is a subclass, and subclass[1] == class[1][1]
.
This works great if I use vanilla Lua ipairs()
to iterate over subclass
, but if I use Pico-8's all()
or foreach()
built-ins, things start breaking. Unlike with ipairs()
, if #subclass < #class
, the loop continues and retrieves any remaining values from class
! So, in the example below, the all()
loop will correctly access class[1][1].foo
on the first iteration, but instead of stopping, it will then try to access class[2].foo
.
EDIT: Forgot to mention, #class[1][1]
/ #subclass1
and/or count(class[1][1])
/ count(subclass1)
report the expected counts, so a C-style for loop would also work.
subclass1 = class:new() subclass2 = class:new() subclass1:new({ foo = "bar" }) -- assertion passes for _, instance in ipairs(subclass1) do assert(instance.foo) end -- assertion fails for instance in all(subclass1) do assert(instance.foo) end |
I don't mind using ipairs()
, but since it does cost an extra token, I'm hoping this is a bug!
hum.. __index is used to chain resolution of table entries.
also adding the child to the parent is odd (what is the functional purpose?).
the ‘standard’ way is:
function class:new(instance) local instance=instance or {} return setmetatable(instance,{__index=self}) **or** self.__index = self return setmetatable(instance, self) end |
@freds72, I got that from the classes page on lua.org (https://www.lua.org/pil/16.1.html). I understood it as a metatable is just a table with all the metamethods like .__index
, .__add
; so the first line sets all the metamethods of instance
to whatever self
is, then the second line resets instance.__index
to refer back to instance
instead of self
? I just tried both and they seem to be equivalent, but yours is one token shorter, so thx for that!!!
I realize adding the child to the parent is odd, but it lets me save tokens by not having to add the object to some other table when I instantiate it, and enables me to save more with stuff like this:
function class:do_method(method, ...) for _, instance in ipairs(self) do if (instance[method]) instance[method](instance, ...) end end function class:spawn() for i=1, 8 do self:new({ num = rnd() }) end end subclass1, subclass2 = class:new(), class:new() function subclass1:print_num() print(self.num) end function subclass2:print_num() print(self.num + rnd()) end --spawn subclass1, subclass2 instances class:do_method("spawn") --print all subclass1, subclass2 nums class:do_method("do_method", "print_num") |
ok but still don’t it in a game context 🤷♂️
also that works only at level 1, any second level class will actually be added to the parent, not that ‘root’ class
@freds72, Sorry, just trying to provide some quickly-digestible examples. In a game context, I've employed this extensively in both my first game and the demo I posted on my Twitter acct yesterday, where I'm using methods similar to the "do_method" example to update all the parallax background layers, boats, rowers, rocks, etc with a single call. It works perfectly as long as I use ipairs() for subclasses where #subclasses < #classes
.
Anyway, for setting metatables/methods, I've just tried all the syntactical variations we've mentioned in this thread and they all behave the same; ipairs()
works as expected and, I forgot to mention initially, a C-style loop would also work since #subclass1
/ #class[1][1]
and count(subclass1)
/count(class[1][1])
report the expected counts.
If it was the other way around, I would be more suspicious of my own code, but I feel like all the assertions in the attached cart should pass?
ok - fair enough, I’d prefer a system where class inheritance has nothing do to with world entities!
the class[1] is awfully confusing (but hey that’s your code!)
@freds72, Oh yeah, it's super confusing! I don't necessarily like it either, but it's saved me a lot of tokens so I've embraced it fully, lol. Anyway, thx so much for trying to assist! As always, I super appreciate your help and advice!
Maybe all() is implemented by going over array elements until a nil element is reached. (Which - your __index case aside - is just as legitimate as going until the length, since the number of non-nil elements at the start of the array is a valid possible value for the array length [unfortunately, not the only valid possible value])
@thisismypassword I hadn't thought of that and it makes a ton of sense. If that's the case, then I believe this would actually be the expected behavior for all()
and foreach
. Thanks for your insight!!
(Marking as resolved, I think this is probably not a bug now.)
[Please log in to post a comment]