Help with metatables & OOP

I’m attempting to conquer Metatables and learn OOP so I’m trying it for a small project. I think I’ve got decent progress so far, however, I’ve gotten stuck.

This represents the problem that I’m having:

--Module Script
local question = {}
question.__index = question

function question.Create()
	local newQuestion = {}
	setmetatable(newQuestion, question)
	
	newQuestion.Property1 = "exampleA" --I define some properties in this function
	newQuestion.Property2 = "exampleB"
	return newQuestion
end

function question:DoStuff() --I can't access the property here
	print(self.Property1) --nil
end

return question
--LocalScript
local questionModule = require(path.to.module)

local question1 = questionModule.Create()

--I can access the properties fine in this script
print(question1.Property1) -- "exampleA"

question1:DoStuff()
My actual code if you need it

Basically, I defined some properties of newQuestion in the Question.Create() function of the module script. Those properties are needed for future reference and they aren’t there when I try to use them from the Question:ToggleVisibility() function.

I’ve removed most of the stuff that isn’t related to ease bloat:

--ModuleScript
Question = {}
Question.__index = Question

function Question.Create(QAProperties)
	local newQuestion = {}
	setmetatable(newQuestion, Question)
	
	QuestionCount = QuestionCount + 1
	newQuestion.Question = QAProperties.Question
	newQuestion.DefaultTransparency = QuestionCount % 2 == 0 and 1 or 0.75 
	newQuestion.IsVisible = true
	
	--This is creates a Frame and some TextLabels, and formats them properly
	newQuestion.QuestionObject = CreateQuestionObject(QAProperties.Question)
	newQuestion.QuestionObject.Parent = QAObjectsHolder --Folder that holds all the questions
	
	AnswerCount = AnswerCount + 1
	newQuestion.AnswerProperties = QAProperties.AnswerProperties
	
	--Same thing here, but for the answer
	newQuestion.AnswerObject = CreateAnswerObject(QAProperties.AnswerProperties)
	newQuestion.AnswerObject.Parent = QAObjectsHolder
	
	--Returns a Vector2 of the size after some calculations
	newQuestion.AnswerSize = CalculateAndSetAnswerSize(newQuestion.AnswerObject)
	Question:ToggleVisibility()
	
	return newQuestion
end

function Question:ToggleVisibility()
	if self.IsVisible then -- These are nil
		self.IsVisible = false
		self.QuestionObject.BackgroundColor3 = Color3.new(0, 0, 0) --Errors because of nil
		self.QuestionObject.BackgroundTransparency = self.DefaultTransparency
		
		local Tween = TweenService:Create(self.AnswerObject, TweenInfo, {Size = self.AnswerSize})
		Tween:Play()
		Tween.Completed:Wait()
	else
		self.IsVisible = true
		self.QuestionObject.BackgroundColor3 = Color3.new(1, 1, 1)
		self.QuestionObject.BackgroundTransparency = 0.9
		
		local Tween = TweenService:Create(self.AnswerObject, TweenInfo, {Size = self.AnswerSize})
		Tween:Play()
		Tween.Completed:Wait()
	end
end

return Question
--LocalScript

--Require the module from above
local FQM = require(script.Parent.FAQQuestionsModule)

--//Questions & Answers
local Q1, A1 = FQM.Create({
	Question = "Question Prompt",
	AnswerProperties = {
		TextLabel = {
			AnchorPoint = Vector2.new(0.5, 0.5),
			--etc...
		}
	}
})

--I can access all of the properties here...
local Questions = {Q1, Q2, Q3, Q4, Q5, Q6, Q7}
local Answers = {A1, A2, A3, A4, A5, A6, A7}

--//Button Activations
local function ButtonActivated(QObject)
	QObject:ToggleVisibility()
end

for _, QObject in ipairs(Questions) do	
	QObject.QuestionObject.ActivationButton.Activated:Connect(function()
		ButtonActivated(QObject)
	end)
end

This has been bothering me for some time now, and I’m assuming I just don’t understand something :confused:

2 Likes

For reference, self in methods is really kinda dumb.
function obj:func(...) is the same as writing function obj.func(self, ...) or obj.func = function(self, ...).
Also, obj:func(...) is the same as calling obj.func(obj, ...).
As for why you cannot access the value, you must be doing something wrong which I don’t really understand as the example code works perfectly fine for me.


Are you sure you are not accidentally doing Question:DoStuff(myQuestion), Question:DoStuff(), or myQuestion.DoStuff()?
Try printing out type(self), typeof(self), and self and seeing what is being sent to the function to debug. If it doesn’t match what you are giving to it (or trying to), then it is not sending the correct object. Also make sure to check some of the properties before you call the method to check their prior values.

Looking back to my original code (not this example), it turns out I was calling the function on the module (question) itself instead of the returned newQuestion (question1).

Thanks, haha :man_facepalming:

It’s ok, we all make mistakes! That is what helpful debugging is for! :dog:

1 Like

What do you mean “self in methods is really kinda dumb?” Accessing self is how object oriented programming works. Table accesses through self are also much quicker.

OP: Try setting the properties of the new question before you set the metatable. Looks like you got it figured out.

Yes obviously but the way that self works in Lua is much more different from how this and other similar class structures in other languages work. Lua has a specific “method call” and the way that classes are constructed are very strange and odd and also make it easier to run into problems. The only main advantage of how this self system works is you can do this with methods:

local kill = game.Destroy

kill(myObject) --myObject:Destroy() or game.Destroy(myObject)

pcall(ds.SetAsync, ds, key, value)

as self is just a parameter that gets inserted at the front, and it can be named anything you want.
You can even use print like a method:

local mt = {__index = {print = print}}
local test = setmetatable({}, mt)
mt:print() --table: 0x......

But in other languages, classes have a much better structure and do not have this magical ‘method call operator.’
Examples:

Python:

class MyNumber:
    def __init__(self, value = 0):
        self.num = value 
    def test(self):
        print(self.num)

yay = MyNumber(2)
yay.test() # no special method call operator

C++:

#include <iostream>
using namespace std;

class MyNumber {
    double num = 0;
    public:
    MyNumber(double value = 0.0) : num(value) {}
    void test() {
        cout << num << endl; 
    }
};

int main() {
    MyNumber thing(2.3);
    thing.test(); // again
}

JavaScript:

class foo {
    constructor(value = 0) {this.num = value;}
};
foo.prototype.test = function() {console.log(this.num);}

var yay = new foo(3);
yay.test();

All I got from what you said is “Lua has __namecall so self is bad.”

None of this changes the fact that, in Lua, when you are attempting to program using OOP, using methods and self is the best way to go.

Again, obviously, but I am just pointing out the fact that because this is how Lua is structured and designed, it has pros and cons, and I was elaborating on what you asked me about so I don’t see what the point of saying that again is.

Your original statement, “For reference, self in methods is really kinda dumb.” led me to believe that you didn’t think this should be standard practice in Lua.

Ah no, I just believe that it seems like Lua was not particularly designed to have good OOP structure like a lot of these other languages who provide much cleaner and better ways (imo) of creating classes and custom objects.

1 Like