All about Object Oriented Programming

As you asked about speed, and as a general warning, this method of replication is not particularly efficient. Things which are convenient come at the price of performance (usually).

In this case you are giving every object a new meta table which isn’t so bad, just uses a bit more memory which we have plenty of nowadays.

However the real problem is now every single access to an object is routed through a proxy meta table as well as a function call (or with it linked to a replication function then even more!).

Practically I am not sure what real world performance impact this would have. However i am sure that if you use oop a lot in a project, you will notice this slow you down.

While this seems like a good idea (and to be honest it is quite a nice tidy idea), it should only be used as a temporary solution.

(Also note that replication like this should attempt to buffer changes. While ROBLOX does do this behind the scenes it has a fair bit of overhead so doing your own buffering is advised. Particularly you should attempt to create “delta frames” which are logs of only the changes made and multiple changes to a variable get condensed into whatever the final value is before the delta frame is sent across the network.)

4 Likes

I appreciate this, though to be honest, I’ve felt for a while now that OOP in ROBLOX does not require metatables, for basically anything.

OOP is traditionally best served on the Java Virtural Machine platform, and the go-to getting/setting of properties is through methods, and not through what might hypothetically be an equivalent to __index or __newindex

That being said, inheritance and OOP in general can be put in a script simply:

local Automobile = {
    staticProperty = 0,
    new = function()
        return {
            numberWheels = 0,
            getNumberWheels = function()return self.numberWheels end
        }
    end;
}

local auto = Automobile.new()
local auto_wheelCount = auto:getNumberWheels()

local Motorbike = {
	
	new = function()
		local bike = Automobile.new() --this is our inheritance
		bike.numberWheels = 2
		bike.canBreak = true --add new properties.
		return bike
	end;
} 

local bike = Motorbike.new()
local bike_wheelCount = bike:getNumberWheels()
local bike_canBreak = bike.canBreak --or you can add a method to return canBreak as we did with numberWheels inside auto

I mean, if you want the added benefits of metatables (like __index and __setindex and __call, among other things) go for it- its just more of a hassle imo

12 Likes

The code snipped I posted has the buffering that I wrote for it taken out for brevity, and I use coroutines to manage the buffering so that it’s in a different thread than the one setting the property.

1 Like

imo i’d use something like middle class, good tutorial either way for people hoping to get into oop

1 Like

Are there any other advantages/disadvantages to this method? I still don’t really get metatables after this and would rather cut them out and use this method.

1 Like

In all honesty I’ve never found something that utilizes metatables that can’t also be done without them. In OOP, and in everything I code, I tend to not use metatables.

1 Like

Metatables (imo) are a half solution in roblox, since you do not have access to all the metamethods available for use, and beyond that- in normal lua it’s cumbersome.

The main benefit to my method is the lack of complexity, its straight-foward code that gets the job done.
The main benefits of using meta-tables over my method is the __index, __newindex, or __call metamethods, but that’s if you value that above the complexity.

Beyond those points the only benefit I suspect of my method is a minuscule performance boost, certainly a negligible one though for almost all situations.
And really as a rebuttal to your ‘I still don’t really get metatables’, it’s really just to allow tables to have meta-methods be triggered when a VM instruction or intention is executed on the table object. It’s Lua’s half-there metaprogramming
You don’t really need to get using them, unless you want a job that depends on heavy Lua knowledge. A hobbyist doesnt absolutely need it, and often its simpler to do without them

1 Like

From my limited knowledge of metatables, you should use them for OOP. Why?

Well, let’s say we were making a gun class using the non metatable way. The gun class consists of some properties and lots of methods. So you use the class in your game and everything is great. But now you want hundreds or maybe thousands of gun objects in the game.

If you use the non-metatable way, you now have hundreds of tables, which is fine but let me get the rest of the sentence, with hundreds of functions. It’s fine to have hundreds of tables with only properties because each object is different, but they all have the same exact functions. Why have hundreds of sets of functions when you can only have one set of functions and call it a day?

This feature of metatables will basically say “hmm, the method ‘shoot’ doesn’t exists in this table, let me check the other table,” effectively having hundreds of sets of properties but only one set of functions (if you set it up that way.)

I guess that this may not matter to you if you don’t have many objects, but I can’t live with the fact of using the other method while I know I can make better and more efficient code using metatables. Plus, it’s only a matter of adding two lines of code. One, setting the __index and two, calling the setmetatable function.

Again, I have limited knowledge of metatables so the above information may be inaccurate (someone check me.)

11 Likes

This is correct. Using meta tables in this way makes function (and variables if you want) definitions effectively static saving on memory (although look up will take slightly longer).

Please note that this tutorial isn’t designed to be the definitive best way of doing oop in Lua. Honestly the best way is whatever let’s you code best and most easily. This tutorial was meant to demonstrate oop without external dependencies and minimal boilerplate code whilst retaining features such as inheritance, operator overloads and (somewhat limited) polymorphism, with minimal impact on performance.

9 Likes

I wont dispute whether or not your information is inaccurate as per your anecdote of thousands of objects- but I understand your point.
Personally, if I was concerned enough with that, I’d simply give each ‘class instance’ a dummy function that points to a function declared in one spot;

To remake my Automobile class:

local Automobile = {
	staticProperty = 0,
	methods = {
		getNumberWheels = function(self) return self.numberWheels end
	},
}
Automobile.new = function()
	return {
		numberWheels = 0,
		getNumberWheels = Automobile.methods.getNumberWheels
	}
end

Now when you use the method from multiple instances of a class- you’ll be calling one function instead of each individual copy in each class
(all this is just to say I don’t think metatables are justified for OOP in general)

1 Like

Out of curiosity, what would you use metatables for if not for OOP related stuff?

1 Like

Extremely rare case that happened once:

I needed a .IndexChanged event for a table once, ended up using a Metatable + bindable for that… But besides that I can’t think of any other use.

1 Like

There are a few others:

  • Weak metatables, using __mode, when you want to keep a reference to something but still allow it to be garbage-collected.
  • Making a table act like a function using __call
  • Handling (new)indexing in some cases. You can do some neat stuff with it.
  • Making proxies around objects. I answered a question on here once with a function that let you set properties of and call methods of multiple objects at once. wrap(workspace.Part1, workspace.Part2).Color = Color3.new(1, 0, 0)
  • There’s certainly other neat things that can be done using metatables that are not OOP.
4 Likes

short answer: Pretty much just meta-programming (example code at bottom)
long answer: its hard to do meta-programming without a ultra-light OOP setup…

It’s good if you want to use those meta-methods to do something like sorting a table, or want to call table like a function (for some reason I cannot name(?)) or want to set an index to a value in a table when that table doesnt already contain said index-

You know, its weird. Metatables in Lua are literally a mixed bag of meta-programming and OOP. They shouldn’t be the means of setting up OOP in Lua- only the means of setting up meta-programming. The reason I say its a mixed bag is because when you try to set up a metatable to use metaprogramming- the way you have to set it up almost already makes it OOP (sans inheritance, or proper OOP workflow)

In a typical situation you just need a table that represents something… anything. Thats why its a mixed bag, because OOP just needs a table that represents anything too, the deference is that using meta-tables very much complicates any OOP implementation, for the small benefit of just having a few usable metamethods (if it wasnt RBX.lua, we’d be having a different discussion). Ill give a OOP Vector3 class (by my standards) and a metatable equivelant, and we can compare it.

I’m also going to shoehorn the __call metamethod in there because thats one of the few useful ones.
And for measure I’m also going to give these classes a means of setting up x,y,z to the best of their talents to better show off how one setup is different from the other.

Metatables will be more robust because of the added stuff, but table-oriented OOP will be cleaner and less convoluted (not to mention way easier to document or understand). [im not intentionally going to make metatables looks bad lol]

local Vec3 = {
	new = function(x,y,z)
		local instance = {}
		
		instance.className = "Vec3"
		instance.x = x
		instance.y = y
		instance.z = z
		
		function instance.magnitude()
			--not using 'self', so i don't need colon.
			return math.sqrt((instance.x^2)+(instance.y^2)+(instance.z^2))
		end		
		
		return instance
	end;
}

local v = Vec3.new(1,1,1)
local length = v.magnitude()
print(length)--1.7320508075689


local Vec3 = setmetatable({},{
	__call = function(tab, x,y,z)
		local instance = {}
		
		instance.className = "Vec3"
		instance.x = x
		instance.y = y
		instance.z = z		
				
		local instance_metatable = {
			__index = function(tab, key)
				if key=='magnitude' then
					return math.sqrt((instance.x^2)+(instance.y^2)+(instance.z^2))
				end
			end
		} 
		return setmetatable(instance,instance_metatable)
	end	
})

local v = Vec3(1,1,1) --__call
local length = v.magnitude --__index
print(length)--1.7320508075689

Its comes down to if you believe the extra-convolution is worth it for the lack of calling ‘magnitude’, or calling ‘Vec3.new’ instead of just Vec3

2 Likes

I find it interesting how you complain about metatables complicating things when the only reason I include them is to simplify code.

Try rewriting a vector3 class in the style that this guide recommends. You might find it a lot more readable as well as convenient.

Edit: Decided to include a quick implementation.

local Vec3 = {}
Vec3.__index = Vec3
Vec3.classname = "Vec3"

function Vec3.new(x, y, z)
	local new = {}
	setmetatable(new, Vec3)	

	new.x = x
	new.y = y
	new.z = z
	
	return new
end

function Vec3:magnitude()
	return math.sqrt(self.x*self.x + self.y*self.y + self.z*self.z)
end

function Vec3:unit()
    local magnitude = self:magnitude()	

	return Vec3.new(
    	self.x / magnitude,
    	self.y / magnitude,
    	self.z / magnitude
	)
end

function Vec3.__add(v1, v2)
	assert(v1.classname == "Vec3", "Attempt to add non vec3 to vec3")
	assert(v2.classname == "Vec3", "Attempt to add non vec3 to vec3")

	return Vec3.new(
	v1.x + v2.x,
	v1.y + v2.y,
	v1.z + v2.z
	)
end

function Vec3.__sub(v1, v2)
	assert(v1.classname == "Vec3", "Attempt to sub non vec3 with vec3")
	assert(v2.classname == "Vec3", "Attempt to sub non vec3 with vec3")

	return Vec3.new(
	v1.x - v2.x,
	v1.y - v2.y,
	v1.z - v2.z
	)
end

function Vec3:__tostring()
    return "Vec3( "..self.x..", "..self.y..", "..self.z..")" 
end

function Vec3.dot(self, other)
  	return self.x * other.x + self.y * other.y + self.z * other.z
end

return Vec3

Which lets you do this

Vec3 = require(game.ServerScriptService.Vec3)

a = Vec3.new(1, 1, 1)
b = Vec3.new(2, 5, 3)

print(a)  --Vec3( 1, 1, 1)
print(b)  --Vec3( 2, 5, 3)
print(a + b) --Vec3( 3, 6, 4)
print(b - a)  --Vec3( 1, 4, 2)
print((a + a):magnitude()) --3.4641016....
14 Likes

Complain is a strong word. OOP and metaprogramming are different, metatables compliment a OOP system, im just contending they should not predicate a OOP system on metatables unless its worth the additional setup to the few whom is concerns.

4 Likes

When using OOP in a module script can I do something like this without the previous data being replaced?

local newTruck = Thing.new("Some name")
local newTruck2 = Thing.new("Some name v2")

Yep. When you call Thing.new it makes a new object and all objects have their own separate set of variables. The only exception is what’s known as static variables where they are shared across all objects.

1 Like

So you’re saying that Truck1’s and Truck2’s data will not be erased. And instead be separate?

If your constructor is set up properly then their data will be separate and won’t be erased.

2 Likes