When I acknowledged the fact that functions are stored as references to them. I thought why we even do module.newindex if functions are anyway a singleton object. So I did this:
export type Class = {
new:(self:Class) -> Class;
}
return {
new = table.clone
}::Class
Inheritence:
local Class:Class = require(Class)
export type Vehicle = {
Speed:number;
Steering:number;
RPM:number;
}&Class
local Vehicle: Vehicle = Class:new()
Vehicle.Speed = 0
Vehicle.Steering = 0
Vehicle.RPM = 0
return Vehicle
I want to ask you what features are missing from this take on OOP that can prevent you from using it
Using metatables would give the Vehicle class access to Class methods without explicitly copying them each time which is inefficient. This setup also does not include a clear mechanism to call parent class methods if a subclass method overrides them.
Suggestions:
Support polymorphism by allowing subclasses to override methods while still accessing parent class methods.
Use metatables for inheritance. This is more efficient and supports polymorphism better.
Add Constructor Logic to customize initialization for each subclass while preserving inheritance.
Encapsulations With Closures: To simulate private properties, you can use closures, which limit the scope of variables.
Example Code:
-- Base Class definition using metatables
local Class = {}
Class.__index = Class
-- Constructor for Class (Base Class)
function Class:new()
-- Private variables using closures
local privateVar = "This is a private value"
-- Public instance creation
local instance = setmetatable({}, Class)
-- Encapsulation: Function to get private variable (emulating private variables)
function instance:getPrivateVar()
return privateVar
end
return instance
end
-- Base class method
function Class:speak()
print("I am a base class object.")
end
-- Vehicle Class Inherits from Class
local Vehicle = {}
Vehicle.__index = Vehicle
setmetatable(Vehicle, {__index = Class}) -- Set inheritance
-- Constructor for Vehicle, which calls the parent constructor
function Vehicle:new(speed, steering, rpm)
-- Call the parent (Class) constructor
local instance = Class.new(self)
-- Add Vehicle-specific properties
instance.Speed = speed or 0
instance.Steering = steering or 0
instance.RPM = rpm or 0
return instance
end
-- Method Overriding: Override the 'speak' method
function Vehicle:speak()
-- Call the base class's method
Class.speak(self) -- Superclass method call
print("I am a Vehicle with speed:", self.Speed)
end
-- Additional method for Vehicle
function Vehicle:drive()
print("Driving at speed", self.Speed)
end
-- Subclass: Car Inherits from Vehicle
local Car = {}
Car.__index = Car
setmetatable(Car, {__index = Vehicle}) -- Set inheritance
-- Constructor for Car
function Car:new(speed, steering, rpm, make, model)
-- Call the parent (Vehicle) constructor
local instance = Vehicle.new(self, speed, steering, rpm)
-- Add Car-specific properties
instance.Make = make or "Unknown"
instance.Model = model or "Unknown"
return instance
end
-- Override 'speak' again in Car
function Car:speak()
-- Call the parent (Vehicle) speak method
Vehicle.speak(self)
print("This car is a " .. self.Make .. " " .. self.Model)
end
-- Create instances and test behavior
local myVehicle = Vehicle:new(60, 5, 3000)
myVehicle:speak() -- Should call Vehicle's version of speak
local myCar = Car:new(120, 10, 4000, "Toyota", "Corolla")
myCar:speak() -- Should call Car's version of speak
-- Access private variables in Class
print("Private var in Car:", myCar:getPrivateVar())
With which I meant that functions are never copied; they are singletons similar to tables, you can check it by using this code:
module
return function()
print("Check")
end
script
local module = PathToModule
local CheckFunction1 = require(module)
local CheckFunction2 = require(module)
print(CheckFunction1)
print(CheckFunction2)
Code above will output same address 2 times
Overriding methods is also quite easy, as well as private variables in case of you really needing them:
local Class:Class = require(Class)
export type Vehicle = {
Speed:number;
Steering:number;
RPM:number;
}&Class
local Vehicle: Vehicle = Class:new()
Vehicle.Speed = 0
Vehicle.Steering = 0
Vehicle.RPM = 0
function Vehicle:new(CustomParam:number) -- override basic Class:new()
local newSelf:Vehicle = Class.new(self)
--Define custom behavior
local privateVar = CustomParam
function newSelf:getPrivateVar() -- bad practice since you create a new function for each of the Vehicle Instances
return privateVar
end
return newSelf
end
return Vehicle
Edit: forgot to mention that my take on oop came from simply thinking of how can you implement oop without metatables
I will mention that table.clone can only shallow copy a table. So if you have any tables as properties you’ll see the following:
local BaseClass = {
new = table.clone,
}
local SomeClass = BaseClass:new()
SomeClass.someTable = {
Hello = true,
}
local foo = SomeClass:new()
local bar = SomeClass:new() -- the pointer to someTable will be the same
foo.someTable.Hello = false
print(bar.someTable.Hello) -- prints false