Switch Statement |
|
Lua lacks a C-style switch
[1] statement. This issue has come up a number of times on the mailing list. There are ways to emulate the same effect as discussed here.
The first question to ask is why we might want a switch statement rather than a comparison chain as such:
local is_canadian = false function sayit(letters) for _,v in ipairs(letters) do if v == "a" then print("aah") elseif v == "b" then print("bee") elseif v == "c" then print("see") elseif v == "d" then print("dee") elseif v == "e" then print("eee") elseif v == "f" then print("eff") elseif v == "g" then print("gee") elseif v == "h" then print("aych") elseif v == "i" then print("eye") elseif v == "j" then print("jay") elseif v == "k" then print("kay") elseif v == "l" then print("el") elseif v == "m" then print("em") elseif v == "n" then print("en") elseif v == "o" then print("ooh") elseif v == "p" then print("pee") elseif v == "q" then print("queue") elseif v == "r" then print("arr") elseif v == "s" then print("ess") elseif v == "t" then print("tee") elseif v == "u" then print("you") elseif v == "v" then print("vee") elseif v == "w" then print("doubleyou") elseif v == "x" then print("ex") elseif v == "y" then print("why") elseif v == "z" then print(is_canadian and "zed" or "zee") elseif v == "?" then print(is_canadian and "eh" or "") else print("blah") end end end sayit{'h','e','l','l','o','?'}
When there are many tests as such, the comparison chain is not always the most efficient. If the number of elements in letters
is M and the number of tests is N, then the complexity is O(M*N), or potentially quadratic. A more minor concern is the syntax redundancy of having "v ==
" for each test. These concerns (minor as they may be) have been noted elsewhere as well
([Python PEP 3103]).
If we rewrite this as a lookup table, the code can run in linear-time, O(M), and without the redundancy so that the logic is easier to modify at whim:
do local t = { a = "aah", b = "bee", c = "see", d = "dee", e = "eee", f = "eff", g = "gee", h = "aych", i = "eye", j = "jay", k = "kay", l = "el", m = "em", n = "en", o = "ooh", p = "pee", q = "queue", r = "arr", s = "ess", t = "tee", u = "you", v = "vee", w = "doubleyou", x = "ex", y = "why", z = function() return is_canadian and "zed" or "zee" end, ['?'] = function() return is_canadian and "eh" or "" end } function sayit(letters) for _,v in ipairs(letters) do local s = type(t[v]) == "function" and t[v]() or t[v] or "blah" print(s) end end end sayit{'h','e','l','l','o','?'}
C compilers can optimize the switch statement in a roughly similar way via what is called a jump table, at least under suitable conditions.[2]
Note how the table construction was placed outside the block to avoid recreating the table for each use (table constructions cause heap allocations). This improves performance but has the side-effect of moving the lookup table further from its usage. We might address that with this minor change:
do local t function sayit(letters) t = t or {a = "ahh", .....} for _,v in ipairs(letters) do local s = type(t[v]) == "function" and t[v]() or t[v] or "blah" print(s) end end end sayit{'h','e','l','l','o','?'}
The above is a practical solution that is the basis for the more elaborate approaches given below. Some are the below solutions are more for syntactic sugar or proof-of-concept rather than recommended practices.
A simple version of a switch
statement can be implemented using a table to map the case value to an action. This is very efficient in Lua since tables are hashed by key value which avoids repetitive if <case> then ... elseif ... end
statements.
action = { [1] = function (x) print(1) end, [2] = function (x) z = 5 end, ["nop"] = function (x) print(math.random()) end, ["my name"] = function (x) print("fred") end, }
action[case](params)
switch (caseVariable) case 1: print(1) case 2: z=5 case "nop": print(math.random()) case "my name": print("fred") end
This version uses the function switch(table)
to add a method case(table,caseVariable)
to a table passed to it.
function switch(t) t.case = function (self,x) local f=self[x] or self.default if f then if type(f)=="function" then f(x,self) else error("case "..tostring(x).." not a function") end end end return t end
a = switch { [1] = function (x) print(x,10) end, [2] = function (x) print(x,20) end, default = function (x) print(x,0) end, } a:case(2) -- ie. call case 2 a:case(9)
Here's yet another implementation of a "switch" statement. This one is based on Luiz Henrique de Figueiredo's switch statement presented in a list message dated Dec 8 1998, but the object/method relationship has been flipped around to achieve a more traditional syntax in actual use. Nil case variables are also handled - there's an optional clause specifically for them (something I wanted), or they can fallback to the default clause. (easily changed) Return values from the case statement functions are also supported.
function switch(c) local swtbl = { casevar = c, caseof = function (self, code) local f if (self.casevar) then f = code[self.casevar] or code.default else f = code.missing or code.default end if f then if type(f)=="function" then return f(self.casevar,self) else error("case "..tostring(self.casevar).." not a function") end end end } return swtbl end
c = 1 switch(c) : caseof { [1] = function (x) print(x,"one") end, [2] = function (x) print(x,"two") end, [3] = 12345, -- this is an invalid case stmt default = function (x) print(x,"default") end, missing = function (x) print(x,"missing") end, } -- also test the return value -- sort of like the way C's ternary "?" is often used -- but perhaps more like LISP's "cond" -- print("expect to see 468: ".. 123 + switch(2):caseof{ [1] = function(x) return 234 end, [2] = function(x) return 345 end })
Yet another implementation of an even more "C-like" switch statement. Based on Dave code above. Return values from the case statement functions are also supported.
function switch(case) return function(codetable) local f f = codetable[case] or codetable.default if f then if type(f)=="function" then return f(case) else error("case "..tostring(case).." not a function") end end end end
for case = 1,4 do switch(case) { [1] = function() print("one") end, [2] = print, default = function(x) print("default",x) end, } end
This one has the exact same syntax as the one above, but is written much more succinctly, as well as differentiates between the default case and a string containing the word 'default'.
Default, Nil = {}, function () end -- for uniqueness function switch (i) return setmetatable({ i }, { __call = function (t, cases) local item = #t == 0 and Nil or t[1] return (cases[item] or cases[Default] or Nil)(item) end }) end
Nil
here is an empty function because it will generate a unique value, and satisfy the [nilpotence] requirement in the return
statement call, while still being having a value of true
to allow its use in the and or
ternary. In Lua 5.2, however, a function might not create a new value if one is present, which will raise problems if you somehow end up using switch
to compare functions. Should it ever come to this, a solution would be to define Nil
with yet another table: setmetatable({}, { __call = function () end })
.
A case-insensitive variant can be made by adding if type(item) == "string" then item = string.lower(item) end
, provided all the keys of the table are done the same way. Ranges could potentially be represented by an __index function metatable on the cases table, but that would break the illusion: switch (case) (setmetatable({}, { __index = rangefunc }))
.
Example usage:
switch(case) { [1] = function () print"number 1!" end, [2] = math.sin, [false] = function (a) return function (b) return (a or b) and not (a and b) end end, Default = function (x) print"Look, Mom, I can differentiate types!" end, -- ["Default"] ;) [Default] = print, [Nil] = function () print"I must've left it in my other jeans." end, }
In the interest of more 'stupid Lua tricks', here's yet another implementation: (Edit: It is necessary that the default functionality is put last in the ... parameter)
function switch(n, ...) for k,v in {...} do if v[1] == n or v[1] == nil then return v[2]() end end end function case(n,f) return {n,f} end function default(f) return {nil,f} end
switch( action, case( 1, function() print("one") end), case( 2, function() print("two") end), case( 3, function() print("three") end), default( function() print("default") end) )
function switch(term, cases) assert(type(cases) == "table") local casetype, caseparm, casebody for i,case in ipairs(cases) do assert(type(case) == "table" and count(case) == 3) casetype,caseparm,casebody = case[1],case[2],case[3] assert(type(casetype) == "string" and type(casebody) == "function") if (casetype == "default") or ((casetype == "eq" or casetype=="") and caseparm == term) or ((casetype == "!eq" or casetype=="!") and not caseparm == term) or (casetype == "in" and contain(term, caseparm)) or (casetype == "!in" and not contain(term, caseparm)) or (casetype == "range" and range(term, caseparm)) or (casetype == "!range" and not range(term, caseparm)) then return casebody(term) else if (casetype == "default-fall") or ((casetype == "eq-fall" or casetype == "fall") and caseparm == term) or ((casetype == "!eq-fall" or casetype == "!-fall") and not caseparm == term) or (casetype == "in-fall" and contain(term, caseparm)) or (casetype == "!in-fall" and not contain(term, caseparm)) or (casetype == "range-fall" and range(term, caseparm)) or (casetype == "!range-fall" and not range(term, caseparm)) then casebody(term) end end end end
switch( string.lower(slotname), { {"", "sk", function(_) PLAYER.sk = PLAYER.sk+1 end }, {"in", {"str","int","agl","cha","lck","con","mhp","mpp"}, function(_) PLAYER.st[_] = PLAYER.st[_]+1 end }, {"default", "", function(_)end} --ie, do nothing })
function switch (self, value, tbl, default) local f = tbl[value] or default assert(f~=nil) if type(f) ~= "function" then f = tbl[f] end assert(f~=nil and type(f) == "function") return f(self,value) end
local tbl = {hello = function(name,value) print(value .. " " .. name .. "!") end, bonjour = "hello", ["Guten Tag"] = "hello"} switch("Steven","hello",tbl,nil) -- prints 'hello Steven!' switch("Jean","bonjour",tbl,nil) -- prints 'bonjour Jean!' switch("Mark","gracias",tbl,function(name,val) print("sorry " .. name .. "!") end) -- prints 'sorry Mark!'
switch
is the wrong model, but that we should look at Pascal's case
statement as more appropriate inspiration. Here are some possible forms:
case (k) is 10,11: return 1 is 12: return 2 is 13 .. 16: return 3 else return 4 endcase ...... case(s) matches '^hell': return 5 matches '(%d+)%s+(%d+)',result: return tonumber(result[1])+tonumber(result[2]) else return 0 endcase
You can provide a number of values after is
, and even provide a range of values. matches
is string-specific, and can take an extra parameter which is filled with the resulting captures.
This case
statement is a little bit of syntactical sugar over a chain of elseif
statements, so its efficiency is the same.
This is implementable using token filter macros (see LuaMacros; the source contains an example implementation), so people can get a feeling for its use in practice. Unfortunately, there is a gotcha; Lua complains of a malformed number if there is no whitespace around ..
. Also result
has to be global.
MetaLua comes with an extension that performs structural pattern matching, of which switch/case is just a special case. The example above would read:
-{ extension 'match' } -- load the syntax extension module match k with | 10 | 11 -> return 1 | 12 -> return 2 | i if 13<=i and i<=16 -> return i-10 -- was 3 originally, but it'd a shame not to use bindings! | _ -> return 4 end
No special handling currently exists for regular expressions string matching, although it can be worked around by guards. Proper support can be added quite easily, and will likely be included in a future release.
Relevant resources:
* Step-by-step tutorial about implementing a pattern matching extension[3], and the corresponding sources[4].
* The latest optimized implementation[5]
local fn = function(a, b) print(tostring(a) .. ' in ' .. tostring(b)) end local casefn = function(a) if type(a) == 'number' then return (a > 10) end end local s = switch() s:case(0, fn) s:case({1,2,3,4}, fn) s:case(casefn, fn) s:case({'banana', 'kiwi', 'coconut'}, fn) s:default(fn) s:test('kiwi') -- this does the actual job
I don't get it. I'm a hard core C/C++ programmer but not once have I longed for switch
in Lua. Why not take this to its extreme and just have Lua parse a real C switch statement? Anyone who can achieve that will learn why it wasn't necessary in the first place. --Troublemaker
How about to avoid: a) a linear search through options, b) to avoid creating garbage every time the case is used, and c) because the if-elseif-else solution is ugly. The condition is repeated N times which obscures and complicates the code. A simple switch on numbers could jump quickly to the code to be executed and doesn't have to generate a closure or a table every time as in the code below.
Actually, I've never used this code as a switch
statement. I thought it made good example code for Lua's features and I may use it one day, but I never have! :-) I think its because you have the associative arrays/tables to map values, so you can design around having to need a switch statement. The times when I do think about needing a switch is when I'm switching on value type. --Alsopuzzled
I've never really needed a switch
in Lua either, but these examples made me bend my brain trying to see why they work. Lua's flexibility continues to amaze me. I'm now that much closer to the ZenOfLua. --Initiate
The issues with these implementations are that they either can't access local variables or they create not just one closure but one closure per switch-branch plus a table. As such, they aren't particular good as substitutes for a switch
statement. That being said, I haven't been suffering too much pain related to the lack of a switch statement in Lua. --MarkHamburg
The lookup table example is a perfectly practical and readable solution that doesn't suffer from those problems at all. It's even mentioned in the page that examples beyond that point are mostly non-practical thought experiments -- Colin Hunt
I am using LUA for scripting my self created web server module. It's terribly fast (and much much faster than my previous PHP version). In here, a switch statement would really be great to case through all GET["subfunction"] possibilities. The only reason is that the only thinkable alternative (if-elseif) is ugly. The other alternatives are as previously indicated very beautiful, world-opening, but a terrible waste of resources. --Scippie
Edit: Maybe I was wrong about the "terrible waste of resources". This is what scripting is all about and the language is made to be handled this way. --Scippie
You don't need a switch statement, if you can just map with a table or use elseifs. The real problem starts when you want to use fallthrough. I am currently working with a database and I need to be able to update it. Since it needs to be updated one step at a time, you jump to the place that updates to the next version and then fall-through until you arrive at the newest version. For that, though, you need either computed goto or a switch statement. Which Lua both lacks. --Xandaros
"@Xandrous There is a goto now"