Log In  

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!

Cart #yujisogeba-0 | 2022-03-11 | Code ▽ | Embed ▽ | No License

P#108418 2022-03-11 03:55 ( Edited 2022-03-11 09:06)

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

see: https://www.lua.org/pil/16.2.html

P#108420 2022-03-11 06:40 ( Edited 2022-03-11 06:46)

@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")
P#108426 2022-03-11 07:58 ( Edited 2022-03-11 07:59)

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

P#108427 2022-03-11 08:23 ( Edited 2022-03-11 08:24)

@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?

P#108429 2022-03-11 09:01

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!)

P#108430 2022-03-11 09:14

@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!

P#108431 2022-03-11 09:39

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])

P#108479 2022-03-12 04:52

@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.)

P#108480 2022-03-12 05:07 ( Edited 2022-03-12 05:09)

[Please log in to post a comment]

Follow Lexaloffle:          
Generated 2022-11-29 00:31:44 | 0.022s | Q:26