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