4

The question originated from http://tylerneylon.com/a/learn-lua/ The tutorial includes codes:

Dog = {dog1 = 'original dog class'}
function Dog.new(self, ... )
    newObj = {sound = 'woof'}
    self.__index = self
    return setmetatable(newObj, self)
end

function Dog.makeSound(self, ... )
    print('I say' .. self.sound)
end

print('Dog=', Dog)
print('Dog.metatable=', getmetatable(Dog))  -- this will output nothing

myDog = Dog.new(Dog)
print('\nmyDog=', myDog)
print('myDog.metatable=', getmetatable(myDog))
myDog.makeSound(myDog)

This is the result of the above codes in tutorial:

wirelessprvnat-172-17-106-141:Programming frankhe$ th test2.lua
Dog=  {
  makeSound : function: 0x0a6cec20
  dog1 : "original dog class"
  new : function: 0x0a6cec00
}
Dog.metatable=  nil 

myDog=  {
  sound : "woof"
}
myDog.metatable=  {
  makeSound : function: 0x0a6cec20
  __index : 
    {
      makeSound : function: 0x0a6cec20
      __index : 
        {
          makeSound : function: 0x0a6cec20
          __index : 
            {
              makeSound : function: 0x0a6cec20
              __index : 
                {
                  makeSound : function: 0x0a6cec20
                  __index : {...}
                  dog1 : "original dog class"
                  new : function: 0x0a6cec00
                }
              dog1 : "original dog class"
              new : function: 0x0a6cec00
            }
          dog1 : "original dog class"
          new : function: 0x0a6cec00
        }
      dog1 : "original dog class"
      new : function: 0x0a6cec00
    }
  dog1 : "original dog class"
  new : function: 0x0a6cec00
}
I saywoof

One additional photo to depict the question more clearly

Although the implementation in tutorial prints ‘I saywoof’ successfully, myDog’s metatable is apparently not as desirable as we expected. Therefore my solution is below (the differences are in Dog.new):

function Dog.new(self, ... )
    newObj = {sound = 'woof'}
    return setmetatable(newObj, {__index = self})
end

The result of my solution:

wirelessprvnat-172-17-106-141:Programming frankhe$ th test2.lua
Dog=  {
  makeSound : function: 0x0d7f2978
  dog1 : "original dog class"
  new : function: 0x0d7f2958
}
Dog.metatable=  nil 

myDog=  {
  sound : "woof"
}
myDog.metatable=  {
  __index : 
    {
      makeSound : function: 0x0d7f2978
      dog1 : "original dog class"
      new : function: 0x0d7f2958
    }
}
I saywoof

My code prints 'I saywoof' and has a more precise table structure. I want to know which implementation is right, the one in tutorial or mine? In addition, I want to know why the code in tutorial generates an iterative definition of Dog's metatable.

Frank He
  • 75
  • 1
  • 8
  • 1
    What `print` function is producing that output? I don't believe it is getting the that hierarchy correct. I believe your change just creates a new table each time `Dog:new()` is called as opposed to reusing the existing `Dog` table. (Though I agree that `self.__index = self` in the `new` function is odd since you really only need to do that once per class (and not once per-instance since `self` is the class). – Etan Reisner May 09 '16 at 02:29
  • I added a photo to describe the question more properly. The `Print` is just the default print function in lua. – Frank He May 09 '16 at 02:47
  • 2
    Both approaches are correct but suboptimal. The one from the tutorial uses a hack to save memory by using the same table for methods *and* metamethods. See first part of [this answer](http://stackoverflow.com/questions/27519842/confusion-of-using-notation-with-index-and-namespace-in-lua/27525100#27525100). Your approach allocates a new metatable for every Dog object. You should allocate the metatable outside of `Dog.new` and re-use it. – siffiejoe May 09 '16 at 02:57
  • Stock lua `print` doesn't print tables like that. Ah, `th` appears to be the torch interpreter with a customized `print` function. And I believe it is just getting confused by the table having itself as `__index` and recursing to its maximum depth of four. If you call `setprintlevel` to change that value I bet you'll see it print exactly as many nested levels as you set it to print to. – Etan Reisner May 09 '16 at 04:46

1 Answers1

6

Lets look at the structure of table Dog, which after construction of a Dog object has a __index metamethod set like this:

Dog = {}            --> table: 000000000079a510     
Dog.__index = Dog   --> table: 000000000079a510

When you print the Dog table the the key __index has the value of its containing table, which leads to the recursion. Standard Lua doesn't pretty print tables, so this print function must stop after ~5 levels (ie: __index : {...} where it halts the recursion). As mentioned by @siffiejoe in the comments, this is a technique to use a single table for both object methods and metamethods.

Concerning which implementation is right; there are many ways to create objects in Lua. The example class, while not wrong, does IMHO needlessly use global variables. Its implementation leaks out into the global environment through Dog and newObj. Not much of a problem in isolation, but when part of a larger program this can be a source of hard to find bugs. An alternative technique is to implement your class as a module. Use local variables for the implementation and export only what is needed to instantiate new objects.

For example, lets look at a refactor of the Dog class:

-- class: dog.lua
--
local Dog = {}     -- the objects implementation
Dog.__index = Dog  -- also, its own metatable

-- instantiate a Dog object:
local function new(name, sound)
    local self = {
        name = name,
        sound = sound or 'woof'
    }
    return setmetatable(self, Dog)
end

-- implement object methods:
function Dog.say(self)
    print(('<%s> says: %s'):format(self.name, self.sound))
end

-- implement object metamethods (unique to Dog objects):
function Dog.__tostring(self)
    return ('Dog: %s'):format(self.name)
end

-- module exports:
return {
    new = new;       -- Dog constructor
    __object = Dog;  -- Dog object table/metatable
}

The module exports a constructor that knows how to build a Dog object without the need for a global object.

-- original example:
myDog = Dog.new(Dog)  --> must pass in the global Dog table to create new objects

-- vs --

-- refactored example:
local Dog = require 'dog'   --> Dog object factory
local myDog = Dog.new()     --> instantiate new Dog

Inheritance can be handled by chaining metatables and calling the parents constructor in the new function:

-- class: colorfuldog.lua
--
local Dog = require 'dog'   -- import the parent class

local ColorfulDog = setmetatable({}, Dog.__object)  -- inherit from Dog
ColorfulDog.__index = ColorfulDog                   -- also, its own metatable

-- instantiate a new ColorfulDog object:
local function new(name, sound, color)
    local self = Dog.new(name, sound)  -- construct the parent first
    self.color = color
    return setmetatable(self, ColorfulDog)
end

-- implement or override object methods:
function ColorfulDog.lookat(self)
    print(('<%s> looks: %s'):format(self.name, self.color))
end

-- implement object metamethods (unique to ColorfulDog objects):
function ColorfulDog.__tostring(self)
    return ('ColorfulDog: %s'):format(self.name)
end

-- module exports
return {
    new = new;
    __object = ColorfulDog;
}

This way each class is encapsulated in its own module which doesn't leak implementation details into the global environment.

-- script: test.lua
--
local Dog = require 'dog'
local ColorfulDog = require 'colorfuldog'

local d1 = Dog.new 'Rover'
local d2 = Dog.new('Max', 'arf!')
local d3 = ColorfulDog.new('Lassie', 'ruff', 'brown')

d1:say()  -- sugar for d1.say(d1)
d2:say()
d3:say()  -- inherited from Dog
d3:lookat()

print(d1, d2, d3) 

Running the above outputs:

$ lua test.lua
<Rover> says: woof
<Max> says: arf!
<Lassie> says: ruff
<Lassie> looks: brown
Dog: Rover      Dog: Max        ColorfulDog: Lassie

Like I said previously, there are many many ways to create classes in Lua, this is just an example of one. However you choose to implement objects, its still a good practice to keep the global environment clean.

Adam
  • 3,053
  • 2
  • 26
  • 29
  • Hi Adam, thank you very much for this very detailed answer! I have another question on http://stackoverflow.com/questions/37377830/lua-attempt-to-index-field-parent-a-nil-value I think you are very likely to be able to answer my question. So please have a look when you are free, thank you! – Frank He May 22 '16 at 18:17
  • Thanks heaps for this succinct example intro into classes and inheritance in lua! – pmckeown Apr 13 '20 at 05:26