Switch // A C++ concept in rich Luau functionality

In C++, you can use a switch statement as an alternative to an if statement… But what about in Lua? Now, with this module, that is possible.

Instead of reading a wall of text explaining this, please watch the below video to learn about switch statements and their functionality in Lua.

You can find the switch module here:

That’s it! Hope this helps :grin:

13 Likes

Heads up! I encourage you mess around with the example code to assist with your learning:

local switch, case, default = unpack(require(script.Switch))

local yardSaleItem = "shoes"

switch(yardSaleItem){
	case("table")(function()
		print("$100")
	end);

	case("chair")(function()
		print("$25")
	end);

	case("toy")();

	case("plate")();

	case("shoes")();

	case("clothing")(function()
		print("$5")
	end);

	default()(function()
		print("something else")
	end)
}

I was actually looking for a resource of this kind since I was learning Java’s switch, but didn’t find any. Thanks!

1 Like

While I’ll like this, im curious why you need a module for something that can be trivilised with a dictionary of functions, this is functionally the same and has less boilerplate code

local yardSale = {
  table = function()
    print("£100")
  end,

  chair = function()
    print("£25")
  end

  default = function()
    print("something else")
  end
}

(yardSale[yardSaleItems] or yardSale.default)()
3 Likes

I explain this in the video, but that is actually something that is the best solution at times. However, at other times, switch statements make more sense.

Switch statements allow you to provide the same output with different conditions.

I recommend you give that video a watch to better understand what I’m saying!

1 Like

Ooh, not a bad idea, I will definitely have to try and implement that soon.

Cases and default statements are their own userdata components, as opposed to be indexes. In other words, this means that it becomes a lot easier to store them in their own locations (such as separate modules).

Also, you can easily store different conditions for the same outcome. In their implementation, that does not exist.

There is a very good reason why you shouldn’t be trying to recreate pseudo-switch syntax in Luau like this. Passing lambda functions to evaluate the conditioning makes the overall computation about 26875 times slower in your example above!

Here’s the benchmarks I did with aggressive optimizations enabled:

Switch

--!optimize 2

local start: number = os.clock()

local switch, case, default = unpack(require(script.Switch))

local yardSaleItem: string = "shoes"

switch(yardSaleItem){
	case("table")(function()
		print("$100")
	end);

	case("chair")(function()
		print("$25")
	end);

	case("toy")();

	case("plate")();

	case("shoes")();

	case("clothing")(function()
		print("$5")
	end);

	default()(function()
		print("something else")
	end)
}

print(os.clock() - start)

Normal If-Statement

--!optimize 2

local start: number = os.clock()

local yardSaleItem: string = "shoes"

local function checkItem(): ()
	
	if yardSaleItem == "table" then
		print("$100")
		return
	end

	if yardSaleItem == "chair" then
		print("$25")
		return
	end
	
	if yardSaleItem == "toy" or yardSaleItem == "plate" or yardSaleItem == "shoes" then
		print("$5")
		return
	end
	
	-- else
	
	print("something else")
	
end

checkItem()

print(os.clock() - start)

Outcomes

Switch: ~2.15 μs
Normal If-Statement: ~0.00008 μs

Yes, it’s easier to read and understand syntax, I get that. But just like micro-optimizations, there’s a point when too much performance gets lost in readability and you need to cut back.

3 Likes

Adding to optimization issues, there is an effortless way to emulate C-style switches as stated below:


Within this post, I would instead state the syntax of this is similar to C++'s because switches are way different than anyone would think, and the following states why (if you’d like to review them).

C++ Switches

Fall-throughs occur since they are not broken out of:

Cross-initializations within cases and default labels:

Multiple options will evaluate the same GOTO of a label:

Etc:


However, I wouldn’t really recommend the use of this as well as other “switch” emulator modules. They can be slow and you really do not want that within code (high performance), especially with games. It could be used with aesthetics though I do like the “Multiple cases evaluates to one output” reference.

Anyways, If you were to create a “switch” with speed and efficiency I would recommend metatablecat’s method (given above) as it’s much faster. It produces way less bytecode than the implementation of the module also. This is a nice reference and module but unsure of its use of it within the production of in-game code.

Im thinking of doing some bytecode analysis between the three different styles I’ve seen here, mine, the OP’s module, and if-chains

Wanted to make my own

local module = {}
module[2] = {Default="default"} -- lazy to style it
table.insert(module, function(var)
   return function(cases)
      if cases[var] then
         cases[var]()
      elseif cases["default"] then
         cases["default"]()
      end
   end
end)

local switch, symbols = table.unpack(module)
switch("davre"){
   Darve = function()
      
   end,
   dave = function()
      print("hi")
   end,
   [symbols.Default] = function()
      print("no switches found")
   end
}

Follow up, I’ll get the bytecode of all three:


If chains for the win with dead code removal optimizations on bytecode instructions:

Source:

Bytecode:
image


Table direct-indexing:
Source:

Bytecode:

Large Output
Function 0 (table):
    4:     print("£100")
GETIMPORT R0 1
LOADK R1 K2
CALL R0 1 0
    5:   end,
RETURN R0 0

Function 1 (chair):
    8:     print("£25")
GETIMPORT R0 1
LOADK R1 K2
CALL R0 1 0
    9:   end,
RETURN R0 0

Function 2 (default):
   12:     print("something else")
GETIMPORT R0 1
LOADK R1 K2
CALL R0 1 0
   13:   end,
RETURN R0 0

Function 3 (??):
    2: local yardSale = {
DUPTABLE R0 3
    3:   table = function()
DUPCLOSURE R1 K4
SETTABLEKS R1 R0 K0
    7:   chair = function()
DUPCLOSURE R1 K5
SETTABLEKS R1 R0 K1
   11:   default = function()
DUPCLOSURE R1 K6
SETTABLEKS R1 R0 K2
   16: (yardSale[yardSaleItems] or yardSale.default)()
GETTABLEKS R1 R0 K0
JUMPIF R1 L0
GETTABLEKS R1 R0 K2
L0: CALL R1 0 0
RETURN R0 0

Module:

Source:

Bytecode:

Large Output
Function 0 (??):
    7:          print("$100")
GETIMPORT R0 1
LOADK R1 K2
CALL R0 1 0
    8:  end);
RETURN R0 0

Function 1 (??):
   11:          print("$25")
GETIMPORT R0 1
LOADK R1 K2
CALL R0 1 0
   12:  end);
RETURN R0 0

Function 2 (??):
   18:          print("$5")
GETIMPORT R0 1
LOADK R1 K2
CALL R0 1 0
   19:  end);
RETURN R0 0

Function 3 (??):
   22:          print("something else")
GETIMPORT R0 1
LOADK R1 K2
CALL R0 1 0
   23:  end)
RETURN R0 0

Function 4 (??):
    1: local switch, case, default = unpack(require("x"))
GETIMPORT R1 1
LOADK R2 K2
CALL R1 1 -1
FASTCALL 53 L0
GETIMPORT R0 4
CALL R0 -1 3
    5: switch(yardSaleItem){
L0: MOVE R3 R0
LOADK R4 K5
CALL R3 1 1
NEWTABLE R4 0 7
    6:  case("table")(function()
MOVE R5 R1
LOADK R6 K6
CALL R5 1 1
DUPCLOSURE R6 K7
MOVE R11 R2
CALL R11 0 1
DUPCLOSURE R12 K14
CALL R11 1 -1
SETLIST R4 R5 -1 [1]
    5: switch(yardSaleItem){
CALL R3 1 0
   24: }
RETURN R0 0

I’ve never read the source code until now and I can confirm it really is inefficient.


You do not need 3 closures to provide a default argument…

There you go metatable, this was tested with the command:
luau --compile=text t.luau

and Luau:
0.54* (Whenever generalized iteration was released)

2 Likes