Using colon instead of dot for functions - justified?

Is this the right category? I was debating scripting support, code review, or dev discussion

PLEASE NOTE: This topic is NOT about the differences of how colon vs dot works. I already know that functions with a colon automatically have self inserted as the first parameter.


Often times when declaring functions in modules, I use the colon instead of dot, even when I dont need to use self. It just looks a lot nicer imo, and is more consistent with built in functions like :GetAttribute, :FindFirstChild, etc.

Examples:

function mapLoading:LoadArea(areaName: string, state: boolean): ()
	-- whatever
end


function dataUtility:GetProfile(): ProfileReplica
	-- get the profile here
end


function playerUtility:ResetCameraPosition(): ()
	-- set camera cframe here
end

Is this actually a good reason to do this though? Is there any real performance drawback to using colons vs dots?

6 Likes

Sure you could.

Personally I would find this confusing as you don’t know if the module is an object or not and needs to be instanced/created first with Object.new().

Performance wise you should measure it yourself using os.clock() probably won’t be more than a 0.0001 ms difference

1 Like

I dont know why I didnt do this earlier lol

Not sure if this is how you are supposed to do a benchmark like this, but here are the results:

Dot: 0.00013589998707175255
Colon: 0.00019570003496482968

So I guess using a colon is like 42% slower?

Code
local module = {}

function module.Dot()
	return
end

function module:Colon()
	return
end

return module


-- in normal script
local mod = require(script.Parent)

do
	local start = os.clock()

	for i = 1, 10000 do
		mod.Dot()
	end

	print("Dot: "..os.clock() - start)
end

do
	local start = os.clock()

	for i = 1, 10000 do
		mod:Colon()
	end

	print("Colon: "..os.clock() - start)
end

It’s fine to use either. They both produce the same outcome. I think it all just comes down to personal preference. As @dthecoolest stated, I think it just aligns more with how Roblox creates instances / objects when you use both:

Instance.new() -- Notice dot notation is used instead of colon

Methods are often what use the colon notation and they are functions that are are members of an object but you can still use the dot notation on the same function to produce the same outcome:

-- These both produce the same outcome
Instance:Destroy() -- Using colon notation
Instance.Destroy(Instance) -- Using dot notation

Like I said, it just comes down to personal preference. I really only use dot notation when creating an object and colon for functions within that object. Performance wise it shouldn’t really matter - the difference is not big enough to cause issues. You’d just be micro-optimizing at that point.

4 Likes

I would recommend against doing this. Consistency is a big deal within programming and using colons for non-classes makes things messy. Use dot notation for non-classes and colons for classes. No exceptions.

Using a colon creates unnecessary self arguments, which will boost memory; not a good thing.

I second this, it is just unconventional to use colon for regular function calls.

I argue the opposite: Using colons the way you’re trying to use it gives people the false assumption that they are working with objects, when in reality they’re not and it’s just a regular module with different functions.

In the Roblox API, colons are only used for object methods, and dots are used for statics that pertain to the class itself.

Are you sure this is the case? I dont think it necessarily “creates” self, it literally just returns itself. Doesnt that mean that:

local module = {}

function module:Whatever()
    print(self == module) -- true?
end

return module

(It does print true in studio)

Using a colon just hides it, the equivalent would be:

local module = {}

function module.Whatever(self)
    print(self == module) -- true?
end

return module

It would be true in the case of module:Whatever(), but if ran with module.Whatever(), it would be nil.

Same goes with

local module = {}
local something = 'egg'

function module:Whatever(somethingElse)
    print(something == somethingElse)
end

return module

It will expect you to always use module:Whatever('egg'), because if you use module.Whatever('egg'), it will register as if somethingElse is nil and self is 'egg'.

1 Like

I know that, but would it really take up extra memory if self is equivalent to module?

Yeah, it still exists. Each variable you create takes up memory.

Throwing my two cents in here. Regardless of the fact that this is Lua (&\Luau) syntactic sugar, it doesn’t mean “use whatever you want”. It should really mean “refer to your style guide”, and in the instance of most large Roblox projects this means referring to the Roblox Luau style guide (which is how Roblox writes its instances, including within locked standard lib metatables [Vector, CFrame, etc…]).

Scrolling down a bit, you will find some basic OOP rules.

  • Constructors are written as .new.
  • Methods that don’t directly operate on your object are wrote in dot notation.
  • Methods that do directly operate on your object are wrote in colon notation (most methods).

self persists regardless, it is only syntactic sugar. The produced bytecode is identical, if you’re curious enough you can pull a compiler and see. The real argument here is how unidiomatic the code becomes when dot gets used for everything. The only time the “performance” difference matters is when the Luau virtual machine retrieves the object. If the object is global in scope it only needs to get pulled once with colon notation, whereas with dot notation it gets pulled twice. Basically, : will never be slower than . ever because colon acts to perform actions only once internally.

1 Like

you should take the averages of many attempts, here’s what I got
image

local funcs = {};

function funcs.dot()
	local start = os.clock();
	
	for i = 0, 999999 do end
	
	return os.clock() - start;
end

function funcs:colon()
	local start = os.clock();

	for i = 0, 999999 do end

	return os.clock() - start;
end

local function compare(n)
	local dotSum = 0;
	local colonSum = 0;
	
	for i = 1, n do
		dotSum += funcs.dot();
		colonSum += funcs:colon();
	end
	
	local dotAvg = dotSum/n;
	local colonAvg = colonSum/n;
	
	print("DotAvg: "..dotAvg);
	print("ColonAvg: "..colonAvg);
	print("Difference: "..dotAvg-colonAvg)
end

print("\nattempt 1")
compare(1000);
task.wait(1);

print("\nattempt 2")
compare(1000);
task.wait(1);

print("\nattempt 3")
compare(1000);
task.wait(1);

print("\nattempt 4")
compare(1000);

warning this lags the game a lot if you’re gonna test it yourself

So again, you don’t need to benchmark this. The differences are well documented.

The instances where dot performs faster in your example only occur when the Luau VM is pulling once regardless of dot or colon notation and you just happened to receive faster performance (basically, they are exactly identical in your example). You are not working with globally scoped objects and therefor you will never see a performance difference. And even if there was a performance difference, you have implemented it wrong for benchmarking given that you should be passing funcs to itself with dot notation (funcs.dot(funcs) to match self).