Encapsulation and Inheritance between module scripts

So, Im trying to learn inheritance in lua, and I’m struggling with what I’m assuming to by some syntaxical stuff but I can’t be sure

--main
local Animal = require(script.Animal)
local AnimalTraits = require(script.AnimalTraits)

local newanimal = Animal.new()
newanimal:NewTrait("hello")
wait(1)
print(newanimal:getTraits())

return 1
--I want this code to be the super class, so all the animals inherit the methods of this class like get traits
local AnimalTraits = require(script.Parent.AnimalTraits)

local Animal = {}
Animal.__index = Animal

local AnimalProtoype = {}
AnimalProtoype.__index = AnimalProtoype

function Animal.new()
	local self = {}
	local newTable = {}
	newTable.Traits = {}
	
	AnimalProtoype[self] = newTable
	return setmetatable(self,AnimalTraits)
end

function Animal:getTraits()
	return AnimalProtoype[self].Traits
end



return Animal

--I want the Animal class to inherit everything everything like newtraits etc.
local AnimalTraits = {}
AnimalTraits.__index = AnimalTraits

function AnimalTraits:NewTrait(newTrait)
	self.Trait = newTrait
	
	return setmetatable(self,AnimalTraits)
end


return AnimalTraits

The first module is the main, then the following two are inside of main. How would I go about inheriting them?

I believe you have the idea of inheritance and such mixed up. In your case it wouldn’t make much sense, and probably overcomplicates the case that animal inherits animalTrait. Rather animal should have a property that is of class animalTrait. The distinction here is that animalTrait would be a property of animal, the object itself wouldn’t inherit the members of animalTrait. Even so, that’s probably overcomplicating it as animalTraits in itself could just be a dictionary.

That being said, it would make more sense that your animal class is your superclass, and that specific animal species inheirt animal. You could maybe then create an even higher class (say organism) that animal inherits, same premises would apply.

Tiger:

local animal = require(script.Parent:WaitForChild('Animal'))

local tiger = setmetatable({}, animal) -- this will inherit all methods in `animal` module
tiger.__index = tiger

function tiger.new()
	local self = setmetatable(animal.new(), tiger)
	self.Striped = true
	
	return self
end

function tiger:Attack()
	if self:GetTraits().Deadly then
		print('The tiger attacked something')
	else
		print('The tiger can\'t attack because it isn\'t deadly!')
	end
end

return tiger

Animal:

local traits = require(script.Parent:WaitForChild('AnimalTraits'))

local animal = {}
animal.__index = animal

function animal.new()
	local self = setmetatable({}, animal)
	self.Traits = traits.new()
	
	return self
end

function animal:GetTraits()
	return self.Traits -- self is the object that is returned from animal.new
end

return animal

AnimalTraits:

local module = {}

function module.new()
	local self = {}
	self.Speed = 5
	self.Deadly = true
	
	return self
end

return module

Some tests:

local animal = require(game:GetService('ReplicatedStorage').Animal)
local tiger = require(game:GetService('ReplicatedStorage').Tiger)

local newAnimal = animal.new()
print(newAnimal:GetTraits()) --> table: { ["Deadly"] = true, ["Speed"] = 5 }
print(newAnimal.Traits) --> same as above
print(newAnimal.Striped) --> nil; the base animal class doesn't have a striped property
print(newAnimal.Attack) --> nil; the base animal class doesn't have an Attack member

local newTiger = tiger.new()
print(newTiger:GetTraits()) --> same as newAnimal:GetTraits()
print(newTiger.Traits) --> same as above
print(newTiger.Striped) --> true
newTiger:Attack() --> The tiger attacked something
newTiger.Traits.Deadly = false
newTiger:Attack() --> The tiger can't attack because it isn't deadly!
2 Likes

Ok I think I understand, I built my code very wrong right from the start. I just had a few questions to clarify about the setmetatables. for the first one in tiger, you do it both inside the constructor or outside by the definition. If I’m not mistaken, the one inside the constructor is so that when you make a tiger instance, since you made its meta table to tiger when you try to do :Attack with it, the index is to itself so It can find it. However, Im just confused by all the setmetatables. This also probably stems from my incorrect definition of setmetatable so correct me if im wrong.
It looks like, when you create a tiger instance, you first have animal.new which returns the metatable for animal. then you add another metatable on top of that, with tiger set to the metatable. Now im just confusing myself, but since the animal.new table already has a metatable referencing animal, why do you need the setmetatable outside of all the functions in Tiger? if you could run it down for me, I am really confused by what __index and setmetatable do now .

1 Like

Okay yeah I probably should have explained what each setmetatable/__index pointer does here.

First off, the __index metamethod is invoked whenever you try indexing a table for an index (entry) that doesn’t exist. So for example if you try doing t.entry, or t['entry'], or t:entry(), those are all examples of what indexing a table looks like, and what may invoke the __index metamethod.

This functionality of allowing tables to point to other tables in the case of nonexistent members is basically what gives you the ability for one object (table) to inherit things from another table.

So for example, if you do:

local t = {}
local othert = {
    someProp = true;
}
othert.__index = othert -- can really be any table you want, doesn't have to be 'othert'

print(t.someProp) -- nil

setmetatable(t, othert)

print(t.someProp) -- true. because when we index t, and find no entry, we then invoke t's metatable's __index metamethod which points back to othert
-- since the entry exists in othert, it will return true instead

So basically the process is as follows when you try to index a table for a nonexistent entry:
1 - check t for an entry ‘someProp’
2 - when t.someProp doesn’t exist, get t’s metatable (othert), and looks for an __index metamethod
3 - if __index is found in the metatable, it either calls the function (if __index is a function), or looks to the table __index points to for the entry
4 - if the entry is found in whatever __index points to, it returns that entry instead

In our animal module:

local animal = {}
animal.__index = animal

function animal.new()
	local self = setmetatable({}, animal)
	self.Traits = traits.new()
	
	return self
end

Suppose we call animal.new(). We now have a new animal object.

What happens when we index it for Traits (do animal.Traits)? It will just return the Traits entry in the animal object.

However, what happens when we index it for GetTraits?

1 - We look in our animal object and see that no entry GetTraits exists.

2 - We get the animal’s metatable, go through the __index metamethod (which points back at the animal module itself) then look in the metatable for a GetTraits member (which is our GetTraits method), then returns that function object.

That’s the premise of “first-level” metatables (just a table with a metatable).

The same premise applies to any number of chained tables.

So this is our tiger module:

local animal = require(script.Parent:WaitForChild('Animal'))

local tiger = setmetatable({}, animal) -- this will inherit all methods in `animal` module
tiger.__index = tiger

function tiger.new()
	local self = setmetatable(animal.new(), tiger)
	self.Striped = true
	
	return self
end

First thing, we call tiger.new() to create a new tiger object.

The constructor itself will create a new object of base animal class, then overwrites its metatable to be tiger. This allows inheritance of properties.

What happens when we index tiger for Striped? Of course, it exists, so it will return true as we expect.

What happens when we index tiger for Traits? Again since tiger inherits animal, all properties from animal will be there. No invocation of metamethods yet.

However, what happens when we index tiger for Attack?

1 - Look in our tiger object, we see tiger.Attack is nil
2 - Get our tiger object’s metatable (tiger module itself) for an __index metamethod which points back at the tiger module.
3 - Look in __index (tiger module) for a member “Attack”
4 - It exists, so we return that function object.

Now, where the part of chained metamethods comes into play. What happens if we try indexing tiger for a member GetTraits?

1 - Look in our tiger object, we see tiger.GetTraits is nil
2 - Get our tiger object’s metatable (tiger module itself) for an __index metamethod which points back at the tiger module.
3 - Look in __index (tiger module) for a member “GetTraits”. It doesn’t exist.
4 - Since step 3 failed, we try to get the metatable of the tiger module itself.
5 - Since tiger module has a metatable, we look in its metatable for an __index metamethod. Instead of pointing back at the tiger module, it points to the animal module
6 - It looks in the animal module for an entry Attack
7 - animal.Attack exists, so we return that function object.

You basically repeat steps 5-7 until you land on a table that doesn’t have a metatable which is how a table can have a metatable that has a metatable that has a metatable and so on indefinitely.

1 Like

Ok this is huge, thank you. Few small things I wanted to clarify, in tiger when you create the instance, you set the table to animal.new() which would return a meta table, so I’m assuming this completely removes the meta table that it was previously pointing to, which would be animal. And one last thing, when creating a tiger you wouldn’t technically need to use animal.new() for the first table correct? This wouldn’t work obviously because you would never have created .trait, but it could still exist and therefore this tiger could still access other animal methods that wouldn’t include .Traits? This is kind of a niche thing idk it is just kind of for my understanding

Right, it gets rid of the existing metatable, then replaces it if the second argument is a table (or if the second argument is nil, it will remove the metatable entirely).

Technically no, you don’t have to- the object could exist without a .Traits property, but if you like the option of allowing inheritance of properties easily without manually redefining the properties in your subclass constructor, and possibly also need the option of indexing the object for a property coming from the superclass, it probably would be a good idea.

Right, assuming your setup is the same, you could still access the methods inherited from animal (GetTraits), but if in your GetTraits method you try to read self.Traits, you would still get nil.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.