Simple switch statement module

Please note that anything that can’t be used as a table index will not work! That includes:

  • Nil
  • Other tables (values contained within those tables will still work, as long as they aren’t nil)
  • Values that depend on the __eq metamethod to be compared, such as CFrame

So I was doing some character CFrame manipulation and I couldn’t use tables to map values to avoid elseif chains, so I made this quick switch statement implementation module in Lua, and I’m sharing it for anyone who needs it!

Source
local Switch = {}
Switch.__index = Switch
setmetatable(Switch, Switch)

local function switch(value: any)
    return setmetatable({
        _value = value,
        _callbacks = {},
        _conditionFound = false
    }, Switch)
end


function Switch:Case(value: any, callback: (any) -> any)
    if (value ~= nil and typeof(callback) == "function") then
        self._callbacks[value] = callback
    end

    return self
end


function Switch:Default(callback: (any) -> any)
    if (not self._conditionFound) then
        callback()
    end
end


function Switch:__call()
    if (self._callbacks[self._value]) then
        self._callbacks[self._value]()
        self._conditionFound = true
    end

    return self
end


return switch

Example:

local switch = require(path_to_switch)

local VARIABLE_TO_COMPARE = 10

switch(VARIABLE_TO_COMPARE)
:Case("someString", function()
    print("first case: " .. VARIABLE_TO_COMPARE)
end)
:Case(11, function()
    print("second case: " .. VARIABLE_TO_COMPARE)
end)()
:Default(function()
    print("default function run")
end)

-- > default function run

Please note that after the end) of the last :Case() there should be two brackets as in a function call!
Enjoy!

11 Likes

Definitely useful, wish this was a feature by default thanks for the contribution!

1 Like

This isn’t better than an elseif chain, inside a function passed to this switch module it cannot return to the function where the switch was called, it cannot break out or continue a loop which the switch is used in, and it cannot use the vararg from the function in which the switch was called from. This module also uses if, so what is the benefit? Just avoiding elseif chains doesn’t seem like a reason to use something like this, considering it must allocate a temporary object to do the switch (and later free that temporary object) and the previously mentioned things which are impossible inside of an inner function.

If mapping values using a Lua table doesn’t work, then using an array and table.find should work (because it uses __eq) fine.

Also, what is the problem with elseif chains?

5 Likes

I overlooked the fact I forgot a _ in _conditionFound in :Case(), and I also didn’t set it to true. I’ve fixed it now.


That is undeniably facts.

Sorry, I can’t understand what you implied here?

It’s more performant when you’re comparing a dozen times (however this obviously doesn’t apply to Lua, and as you said:

this switch module is definitely less performant than an elseif chain, although I believe it would only be noticeable for a task running every few miliseconds), and readability, although that’s a matter of opinion, however most people I’ve seen on the internet see elseif chains as hard to read code, and I believe that’s quite fair.

I believe it’s because of backwards compatibility with old code, they could make it not a keyword like they did with continue, however, I believe it would be hard to implement on the parser, especially when they’re working on type checking at the moment.
Also, thanks for praising me, it really means a lot, even if it’s just a single person! ^^

This module does the same thing as an elseif chain would do, it checks the conditions in succession. This module even has to check if it has already reached a valid case, to see if it should check the condition. When people say that switch is faster, they are referring to that it be optimized to a jump table (dynamic goto).

Another difference between this module and elseif chains is that the values being compared against must be evaluated even if a branch is reached.

local x = 1
if x == 1 then
	print(x)
elseif x == error"a" then
	print(x+1)
end

This outputs 1, and the second condition is never checked. Using this module,

local x = 1
switch(x)
:Case(1,function()
	print(x)
end)
:Case(error"a",function()
	print(x+1)
end)

This outputs 1 and then errors.

2 Likes

Switch statements = if, elseif, else

This is literally a switch statement in “if” syntax:

if (condition) then

elseif (condition) then

else

end

Then let’s compare an, I don’t know C/C++/Java/JS switch statement:

switch (condition) {
    case 1:
        break
    default: 
        break
}

What’s the difference, looks?
And I am positive else if statements would be somewhat more efficient than this in some way.
Even table switch statements.

That is indeed unwanted behaviour which could lead to some bug, but to be fair, who would use error() in an if statement?

In essence, yes, in practice, no.
I’ve discussed here how many people find a switch statement more readable than an elseif chain.

If you don’t trust my word on what I’ve replied to @Halalaluyafail3 about the efficiency of switch statements over elseif, then I suggest reading this StackOverflow question. It’s about the C# implementation, however I believe it should be similiar to the other languages that use it.

It means that conditions can’t rely upon previous conditions.

local x = 1
switch(x)
:Case(foo(),function()
	print(1)
end)
:Case(bar(),function()
	print(2)
end)

In this example, if bar() errors if foo() == 1, and foo() == 1, it still does bar() and errors.

Usually you won’t call error in a condition, something that might happen more often would be

-- o is a child of f
-- f has either a child named 'a' or a child named 'b'
-- f always has a child named 'c'
-- if a exists, o must be a
switch(o)
:Case(f:FindFirstChild"a",function()
	print"o == f.a"
end)
:Case(f.b--[[if a exists this still gets evaulated, and errors]],function()
	print"o == f.b"
end)
:Case(f.c,function()
	print"o == f.c"
end)
:Default(function()
	error"unreachable"
end)
1 Like

I believe that I’ve now fixed that sacrificing practicality as the last case should be called as a function, which now checks for the value given in the _callbacks table.
There’s nothing to be done about the inability to break, continue, etc. though, I believe.

This doesn’t fix the problem, as all of the potential values for the switch still have to be evaluated. The most feasible way to fix it would be to pass a function which generates the value to be compared against, but that would result in a very verbose syntax.

There are also some disadvantages to storing the cases as keys in a table: using nil as a case doesn’t work, using NaN as a case doesn’t work (although this doesn’t make much sense), and __eq won’t be checked.

switch(CFrame.new())
:Case(CFrame.new(),function()
	print(1) -- never gets outputted, as the values are referentially unequal
end)()
2 Likes

I see; so this module is kinda useless after all. I guess I didn’t plan ahead. I’ll just leave a warning then, I don’t feel like taking down this thread as this discussion we had is valuable learning material ^^

1 Like

I didn’t mention any language in the comparison of what I stated.
I did say “this” which means the module.
And elseif as in elseif.

That’s looks not functionality.

My bad, I thought you were saying about switch statements in general.

That’s very subjective to opinions. I don’t think there’s a point discussing this.

I don’t really get your point. Yes, that’s looks, not functionality, and what you imply by that? That it’s bad?

I didn’t say anything about it’s bad. I stated that looks are better with switch statements, functionality doesn’t change between them. It still does the same job as an if statement.

1 Like

Can I have an explanation to why the final case should have another two brackets at the front?

1 Like

Because the checks to see what callback to be run are made on the __call metamethod:

(by the way, __call runs when the table is called like a function, thus why the two brackets at the last :Case())
:Case() will simply put the callback under the value in the _callbacks table:

It’s just the best hacky way I found of doing it. (^▽^)

1 Like

iirc only in languages like c# where they are compiled is where you use switch statements cause in compiled languages it has to go through each else if statement to check for type errors/ compile but in interpretted languages it “arrives” to it. You can test this by writting a c++ program with if statements inside it would produce an error and do the same in lua. Lua will still run until it reaches that if statement

1 Like

Yup. The compiler does that because if the code is wrong, the code it produces is wrong too, hence the compiler is a nice guy and tells you it’s wrong (runtime errors still exist though). While interpreted languages compile to bytecode and then “catch” errors on execution.

If you really want to do this, just use a table:

 local tab = {
     ["SomeValue"] = function() return 1 end
     ["SomeOtherValue"] = function() return 2 end
}
local var = "SomeValue"
if tab[var] then
   tab[var]()
else
  -- this would be the default case
end

You could of course put this away in a module. In the end, else ifs should work fine. If you have a super long list of things, I recommend using a table: the keys being the cases, the values being functions to be executed.

Here is a good article going a little more in depth:
http://lua-users.org/wiki/SwitchStatement

1 Like