Preformance and Code suggestions for my Switch Statment

Hello, I have made a fully functional Switch Statement that is used in languages such as Java, c++, etc…
Since I want it to be as optimized, organized, and efficient as possible, I am asking for various suggestions on the code

local SwitchStatment = {}
SwitchStatment.__index =  SwitchStatment

SwitchStatment.__call = function(self, case)
	if case == nil then
		case = self.DefaultCase
		assert((case ~= nil), "THERE IS NO DEFAULT CASE SET; CASE ENTRY BLANK")	
	end
	goThroughCase(self, case)
end


--Private Function
function getCaseOrder(self, index)
	local caseIndexOrder = self.CaseIndexOrder
	local currentCaseOrder = caseIndexOrder[index]
	assert((currentCaseOrder ~= nil), string.format("THE CASE %s IS NON EXISTANT!", index))
	return currentCaseOrder
end

--Private Function
function getCaseIndex(self, order)
	local caseOrderIndex = self.CaseOrderIndex
	local caseIndex = caseOrderIndex[order]
	assert((caseIndex ~= nil), string.format("THERE IS NO CASE THAT TAKES THE PLACE OF %s!", order))
	return caseIndex
end

--Private Function
function getNextCaseOrder(self, currentCaseIndex)
	local newCaseOrder = getCaseOrder(self, currentCaseIndex) + 1
	if newCaseOrder > #self.CaseOrderIndex then return nil end
	return newCaseOrder
end

function goThroughCase(self, case)
	local caseFunction = self.Cases[case]
	local breaked = caseFunction()
	if breaked then return end
	local nextCaseOrder = getNextCaseOrder(self, case)
	if nextCaseOrder then
		local newCase = getCaseIndex(self, nextCaseOrder)
		goThroughCase(self, newCase)
	end
end

function SwitchStatment:GetCase(index)
	local cases = self.Cases
	local case = cases[index]
	assert((case ~= nil), string.format("THE CASE %s IS NOT DEFINED!", index))
	return case
end

function SwitchStatment:AddCase(index, func, breakOnFinish)
	local cases = self.Cases
	local caseOrderIndex = self.CaseOrderIndex
	local caseIndexOrder = self.CaseIndexOrder
	
	assert((cases[index] == nil), string.format("THE CASE %s IS ALREADY DEFINED!", index))
	
	local order = #self.CaseOrderIndex+1
	caseOrderIndex[order] = index
	caseIndexOrder[index] = order
	cases[index] = function()
		func(index)
		return breakOnFinish
	end
end

function SwitchStatment:SetDefaultCase(index)
	local cases = self.Cases
	local case = cases[index]
	assert((case ~= nil), string.format("THE CASE %s IS NOT DEFINED!", index))
	assert((self.DefaultCase == nil), string.format("THE DEFAULT CASE %s IS ALREADY DEFINED!", index))
	self.DefaultCase = index
end

function SwitchStatment.new()
	local self = setmetatable({}, SwitchStatment)
	self.DefaultCase = nil
	self.Cases = {}
	self.CaseOrderIndex = {}
	self.CaseIndexOrder = {}
	return self
end

return SwitchStatment

Here is a RBXL File SwitchStatment.rbxl (19.7 KB)

This is an interesting bit of code, but I am curious as to whether using this implementation actually saves any computational power over using an elseif chain? It looks like a lot of overhead

It won’t, but that’s not the point of doing this.

1 Like

Nice :slight_smile: Can you show an example of using your switch statement? Also, how would one make it available as a global function?

1 Like

I feel like a better way to do the above code is this:

local switch = function(case)
	return function(cases)
		local shouldBreak = nil
		local default = cases["default"] or function() end
		cases["default"] = nil

		for _, possibleCase in next, cases do
			if possibleCase[1] == case or shouldBreak == false then
				shouldBreak = possibleCase[2]() 
				if shouldBreak then
					return
				end
			end
		end

		default()
	end
end

Example of it in action.

for i = 1, 4 do
	switch(i)
	{ -- Since it returns a function, we invoke it again.
		{
			1, -- Case
			function() -- Expression
				print(1)
			end
		};
		{
			2,
			function()
				print(2)
				return true -- "Break"
			end
		};

		default = function()
			print("defaulted")
		end
	}
end

--[[
	Printout:
	-> 1
	-> 2
	-> 2
	-> defaulted
	-> defaulted
]]

You can always inverse how the arguments are sent. So, you can create a premade switch case then call it throughout your code. That’s more efficient as you aren’t creating the same closures over and over again.

-- Assuming you switched the order the arguments are passed.
local switchStatement = switch {
	{
		1, 
		function()
			print(1)
		end
	};
	{
		2,
		function()
			print(2)
			return true
		end
	};

	default = function()
		print("defaulted")
	end
}

for i = 1, 4 do
	switchStatement(i) -- Same result, but not creating so many function closures.
end

I don’t know how to do this yet because I’m a noob to Lua but I would have thought the simplest way to do something like a switch statement would be something along the lines of…

local switchOptions = {
   ["case1"] = function()
      print("case1")
   end;
   ["case2"] = function()
      print("case2")
   end;
   ["default"] = function()
      print("default")
   end;
}

local selectedOption = "case2";

if switchOptions[selectedOption] ~= nil
   switchOptions[selectedOption]()
else
  switchOptions.default()
end

As I mentioned I don’t know Lua that well so I’m making this up, but I’m betting something along those lines would be the simplest and most optimal.

It isn’t a switch statement, it’s a literal look-up table (though that’s what switch statements are behind the scenes). I think that’d work out better though.

1 Like

Also, you can use the table _G to make it globally available!

Using _G is very bad style.

A key property of good, maintainable code is to reduce the scope of everything, effectively minimizing complexity while maximizing readability, developer efficiency, and maintainability.

Instead, use ModuleScript instances.

Overusing _G is a very bad idea, using it carefully isn’t.

Reasoning
Though a switch function isn’t the best example, it makes sense to expose it globally. It is most likely that it’ll be used throughout the majority of the scripts used. So, cut the need for require and simply using _G.

There is a reason why the standard libraries are globally available in standalone Lua (string, math, io, coroutine, debug). If you know that a module/value is near-universal, might as well make it available globally.

Besides that, in Roblox, _G fields that aren’t loaded by the engine can’t be referenced implicitly. So, it isn’t possible that you’ll be polluting your script’s scope inadvertently.

Though
A better example would be a table of utility functions exposed globally instead of a singular function. It should still exist as a module so that it is still accessible while being transparent where _G.something came from. Thus, while it bleeds beyond its original script’s scope (which, it was going to either way), it is much more readable and efficient.

_G should not be used everywhere and used to make implicit states and pollute _G that should be stuck in its own scope. It should be minimally used for commonly used, top-level variables.

1 Like

@utrain I like your idea of improving the switch statement as I have never seen the method of lua you have used. The switch statement was originally so post to be some optimized way of dealing with tons of if statements; its purpose is to be initialized and then called upon when necessary, but not redefined.

What I mean is this; You would define the switch at the start of your code during its initialization. Then you would call upon it when necessary.

Since this switch is created at a high level language that has to go through the intrepretor, this switch is useless and it is more effecient to use a yandere dev style stack of if statements. I wish Lua contained such a feature.

Thanks for the compliment. But, I wanted to expand what you said.

You’re correct that a if…else…if… statement would be faster than the switch-statement I made. However, I’d argue it isn’t really a switch-statement, just a pretend version. A switch-statement really can be thought of as a look-up table of memory locations to jump to. But, we don’t have those so, we can do this instead:

local switchCases =
{
    [1] = function() print("Case 1") end;
    [2] = function() print("Case 2") end;
    [3] = function() print("Case 3") end;
}

i = 1
switchCases[i]()

Pretend the functions are those jumps!

Now, note that an invalid case (like 4) wouldn’t default. However, it’s time complexity is now an O(1) instead of an O(n) from before, so it’s now better to use a look-up table instead of nested if … else if … statements. But, it means no breaking. I think that’s an acceptable compromise. Daresay, an advantage but, besides the point.


However, there is a solution to the lack of defaults (plus a way to make life easier)!

local switchCases =
{
    [1] = function() print("Case 1") end;
    [2] = function() print("Case 2") end;
    [3] = function() print("Case 3") end;
}

setmetatable(switchCases,
{
    __index = function(tbl) return rawget(tbl, "default") or function() end; -- Only used when nil.
    __call = function(tbl, case) return tbl[case]() end; -- Might as well use
}

-- That'll do.
local i = 1
switchCases(i) 
-- equal to: ((switchCases[i] or switchCases["default"]) or function() end)()
switchCases[i]() -- This works too.

Now, the plus is that you don’t just have to use numbers. You can use any datatype since Lua is dynamically-typed after all.

1 Like

I originally designed it to be that way of where it can use any data type, anyways, thanks for your suggestions.

Code looks decent enough for the Switch Statement Module. However no ones mentions your constructor function.

function SwitchStatment.new()
	local self = setmetatable({}, SwitchStatment)
	self.DefaultCase = nil
	self.Cases = {}
	self.CaseOrderIndex = {}
	self.CaseIndexOrder = {}
	return self
end

This is considered bad practice. I’d suggest doing it like this to reduce that extraneous indexing:

   function SwitchStatment.new()
            return setmetatable({
                  DefaultCase = nil,
                  Cases = {},
                  CaseOrderIndex = {},
                  CaseIndexOrder = {}

            },SwitchStatement)
    end

The benefits of this.

  • We have reduced all indexing.
  • We have reduced all scope referencing.
  • We don’t have to write ‘self.’ every time to add a new element.
  • We have a clean scope
  • We don’t need to allocate a whole variable on the stack to reference this by when we were returning it in the first place.
  • Most important of all, we don’t impact any readability.
  • This would be more optimal.