Search code examples
classluainstantiationlua-tablemetatable

Lua, which way to do class-table and instantiation?


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.


Solution

  • 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.