Combatting else-if chains with tables

Surely at some point we’ve been there: an endless chain of elseifs that goes on seemingly forever. Not only does it look un-neat and unprofessional, it can have some performance hits as well. Today I’ll be covering a solution that, from my experience, not many people know about, despite how useful it is.

Lets start with this elseif chain:

if Text == "abc" then
-- code
elseif Text == "123" then
-- code
elseif Text == "def" then
-- code
elseif Text == "456" then
--code
elseif Text == "ghi" then
--code
elseif Text == "789" then
--code
end

This may be slightly over-exaggerated, but let’s get to the point. We’re gonna start by making a table, with 6 values all with the names of the text above, and assigning them all functions. If you don’t get what I mean, look here:

local TextTable = {
   ["abc"]  = function()
          --code
   end,
   ["123"]  = function()
          --code
   end,
   ["def"]  = function()
          --code
   end,
   ["456"]  = function()
          --code
   end,
   ["ghi"]  = function()
          --code
   end,
   ["789"]  = function()
       --code
   end,
}

I have also filled some placeholder text to represent code, so you may put whatever your code is in there.

Now it’s time for our- drumroll- singular if statement! I’ll show you what I mean now.

if TextTable[Text] then
   TextTable[Text]()
end

Let’s break it down. The if statement scans through all values within TextTable, and if it finds a value with a name that matches Text, it runs the function stored in that particular value within the table, hence the ().

Simple, eh?

This is my first tutorial, I’d appreciate any criticism or suggestions. Thank you for reading.

8 Likes

this is improper and would never run. You probably meant

if TextTable[Text] ~= nil then
2 Likes

This seems inefficient and afaik would cost more memory to implement than a normal else-if chain.

3 Likes

I mean it ran in studio successfully…

I don’t see much of a benefit here.

Yes, it is much more readable, but the fact that you have to add [ ] around every string, type out an additional line and deal with a wasted variable name introduces more issues than solutions.

There is also a questionable performance impact. While calling this once in a script might not do much, every one of those functions will have to be initialized every time you pass through that part of the script, unlike an if else statement. The table lookups and function calls don’t aid much either.

if TextTable[Text] ~= nil then
This isn’t what he should be doing he should just be doing
if TextTable[Text] then
as for your one if the thing is just false instead of nil then it’ll error whereas my one checks if it exists and if so proceed

On an unrelated note to the above,
I would prefer to use short circuit evaluations instead as if you need values from the function then you would have to make the function table within the scope you’re trying to use,

so you could just do instead of the if statement
local pin, toe = TextTable[Text] and TextTable[Text]()
This would allow you to return values from the function to be used where you called the function
:grinning:

you can’t do if Value = Value then, it has to be if Value == Value then

you need a double equal sign

This is probably like unnecessary, sure it reduces code, but you could see the long list in the table still. Also, creating a lot of functions might hog up lots of memory.

Thanks for everyone’s feedback, now that I read through the replies and think through it more, it’s a pretty bland solution that doesn’t change much.

The idea behind what you’re doing isn’t bad. But in this case, in this specific thing it doesn’t make much of a difference.

What someone will usually have is the same code but it changes depending on a comparasion, in that case what you would do is something like this:

local PossibleOutcomes = {
    {
        ValidInputs = {"abc", "bpd"},
        Result = "Something"
    }
}

local input = ...
local result = nil
for _, outcome in ipairs(PossibleOutcomes) do
    if table.find(outcome.ValidInputs, input) then
        result = outcome.Result
        break
    end
end

This example above is like how you would wanna do overhead uis for example, but something like this instead:

local Ranks = {
    {
        Name = "Owner",
        Color = Color3.fromRGB(255, 0, 175),
        UserIds = {123456789},
        Condition = function(player / userId)
            return (if player is in group rank)
        end
    },
    {
        Name = "Player",
        Color = Color3.fromRGB(255, 255, 255),
        UserIds = {},
        Condition = function()
            -- This is the last possible rank, so we always say that it's valid for the player
            return true
        end
    },
}
3 Likes

It has much higher readability, looks a lot better, and a able can easily be converted to a module script, unlike an elseif chain.

Imo, this is not visually appealing nor does it have better readability. With a chain, you can directly see the results of the condition instead if having to refer to a table every time.

Even though I am just a starter in scripting, I will have to agree with this one. The traditional elseif chain is much better in terms of readability.

1 Like

Why bicker about it? Just benchmark it.

The code I used
function testA(Text)
	if Text == "abc" then
	-- code
	elseif Text == "123" then
	-- code
	elseif Text == "def" then
	-- code
	elseif Text == "456" then
	--code
	elseif Text == "ghi" then
	--code
	elseif Text == "789" then
	--code
	end
end

local TextTable = {
   ["abc"]  = function()
          --code
   end,
   ["123"]  = function()
          --code
   end,
   ["def"]  = function()
          --code
   end,
   ["456"]  = function()
          --code
   end,
   ["ghi"]  = function()
          --code
   end,
   ["789"]  = function()
       --code
   end,
}
function testB(Text)
	return TextTable[Text]()
end

function testC(Text)
	local TextTable = {
	   ["abc"]  = function()
			  --code
	   end,
	   ["123"]  = function()
			  --code
	   end,
	   ["def"]  = function()
			  --code
	   end,
	   ["456"]  = function()
			  --code
	   end,
	   ["ghi"]  = function()
			  --code
	   end,
	   ["789"]  = function()
		   --code
	   end,
	}
	return TextTable[Text]()
end

local cases = {"abc", "123", "def", "456", "ghi", "789"}
local start

start = os.clock()
for i = 1,10000000 do
	testA(cases[i%6 + 1])
end
print("Test A", os.clock() - start)

start = os.clock()
for i = 1,10000000 do
	testB(cases[i%6 + 1])
end
print("Test B", os.clock() - start)

start = os.clock()
for i = 1,10000000 do
	testC(cases[i%6 + 1])
end
print("Test C", os.clock() - start)

Test A is with the if-else chain.
Test B is with the table if it is created only once.
Test C is with the table if it is recreated every time (which is the way @nooneisback thinks it has to be done)
Each test function is called 10 million times.
There might be some influence from the way the string is chosen.

Results:

Studio (Luau)

Test A 0.82050225022249
Test B 1.5509403150645
Test C 8.4669149238034

Luvit (LuaJIT on 5.1)

Test A  0.231
Test B  0.454
Test C  11.828

Lua 5.3

Test A	2.4510000000009
Test B	1.9739999999874
Test C	10.441999999981

With 6 items, the if-else chain runs faster, is easier to understand and takes less memory.

Let’s try it again with 500 items.

The rough outline of the code I used (not the whole thing, which is 1024 lines)
function testA(Text)
	if     Text == 001 then
	elseif Text == 002 then
	-- ...
	elseif Text == 499 then
	elseif Text == 500 then
	end
end

local TextTable = {
   [001] = function() end,
   [002] = function() end,
   -- ...
   [499] = function() end,
   [500] = function() end,
}
function testB(Text)
	return TextTable[Text]()
end

local start

start = os.clock()
for i = 1,10000000 do
	testA(i%500 + 1)
end
print("Test A", os.clock() - start)

start = os.clock()
for i = 1,10000000 do
	testB(i%500 + 1)
end
print("Test B", os.clock() - start)

Results:

Studio

Test A 19.097824928002
Test B 1.1308720872039

Luvit

Test A  2.005
Test B  53.229
EXCUSE ME?

I really don’t know why it does that.
It took twice as long when I rewrote the test to use string keys.
It took 14.011 seconds with a table.find-like thing (for i = 1, #asdf) thing in Luvit, 71 seconds with the same in Studio and 27.827956805297 with table.find in Studio (return d2[table.find(d, Text)]()).

Lua 5.3

Test A	62.782000000007
Test B	1.9919999999693

So why use tables instead of if-else chains?

The difference between the two is that if-else chains have to check each candidate individually. This means that if you have a case that the if-else chain doesn’t handle, then it must be compared to all of them to find that it doesn’t match.
On the other hand, tables are hash maps. This means that the key is hashed and the result is looked up much like in an array. Even in a table with a trillion keys, only a few comparisons need to be done; all the other keys are ignored.

In more formal terms, the if-else chain is a linear-time algorithm (O(n)).
As the amount of items to compare to increases, so does running time. It’s fast with a few items, but slow with many items.

The table approach is a constant-time algorithm (O(1)).
The running time stays constant regardless of the amount of items.

The reason why the table approach is slower is because it needs to hash the string to find the result, so there’s some overhead to it that the if-else chain doesn’t. Oh yeah, the function call, too.

So for performance, use an if-else chain if you have a few choices and a table/map of functions if you have a very large amount of cases.

Another difference is that tables (hashmaps…) are limited to exact matches only.
If you want to do one thing if Text is “asd”, another if it’s “ghi” and a third thing if it starts with “jkl”, you can’t do that with tables only; you have to check that with an if string.match(Text, "^jkl").

The most compelling reason to use tables of functions instead of if/else chains is modifying your code dynamically.
You can add and remove from the table of functions. This allows module systems and similar. Most popular admin command systems past Person299’s probably do this.

TL;DR YandereDev’s code is bad, but don’t write your code differently just to blindly avoid his mistakes. You should be able to explain why you’re taking one approach over another.

15 Likes

The following explanation is about the space and time complexity of both options. O(1) stands for one operation, while O(n) represents some amount of operations.

Using a table is useful for indexing a lot of data as @Eestlane771 explained and tested. The reason is that there is a significant difference in time complexity between if-else chains and table indexing. if-else chains have a time complexity of O(n), while indexing a table only costs O(1).

Also, there is a difference in space complexity. The table already contains all options, while the if-else chain only loads the possible options chronologically. That is, until it finds a match. Therefore, the table approach has a space complexity of O(n) and the if-else chain has O(n) at its worst.

I do not think that using if-else chains is bad in general. It is not that bad at all if there is not too much data to choose from. But lets say, you write an admin commands system that contains over 200 commands. Do you really want to have a time complexity of O(200) instead of O(1)? This is a case where tables are useful to improve performance.

4 Likes

Yep, another advantage is that one can loop through the table to get a list of admin commands (Then cache it) hence the dynamic programming to your example.

However I have a big problem with these statements that are being said throughout:

Is this memory usage actually significant? Will it impact performance in a significant way?

From my understanding in programming sometimes it’s ok to sacrifice memory in order to achieve a performance boost which is what the table method does (IF only there are a lot of entries within the table) and other algorithms like FloodFill with a datastructure and quadtrees/octrees.

Even then with only 6 functions, that doesn’t seem too significant is it?

The explanation is incomplete at the moment which I dislike. I would like to know the full picture.

For now I will choose which one seems the most readable and organizable depending on the situation.

1 Like