Lua言語でテーブルを用いてswith-case文を疑似的に実現する


はじめに

バーチャルキャストでユーザーが自由に作成し、VR空間に持ち込むことが可能なアイテム、VCIアイテム
VCIアイテムはLua言語のスクリプトを組み込むことで、アイテムに機能を実装することが出来ます。
lua言語はスクリプト言語でかつ、動作も高速と非常によい言語ではあるのですが、swith-case文がない!!!

そのため、アイテムの名前やコメント内容で処理を分岐させる場合、以下のようにif文で処理を分岐させる必要があり、if-elseが大量になったり、ネストが深くなったり、複数箇所に同じ条件文を書く場合修正ミスが発生したりします。

function onMessage(sender, name, message)
    if name == "comment" then
        local str = nil

        -- カメラを制御するコメントが受信したら、VCI内の特定アイテムを操作する
        if message == "カメラ1" or message == "camera1" or message == "Camera1"  or message == "Cam1" or message == "cam1" then
            str = "camera1"
        elseif message == "カメラ2" or message == "camera2" or message == "Camera2"  or message == "Cam2" or message == "cam2" then
            str = "camera2"
            -- カメラ3,4,5についても同様のif文が必要
        end

        -- strがnilの場合(if文の条件にヒットしなかった)、処理しない
        if str ~= nil then
            src = cameraModule.GetPosition()

            local item = vci.assets.GetSubItem(str)
            dst = item.GetPosition()
            start = vci.me.UnscaledTime
        end
    end
end

判定条件が少ない場合は問題ありませんが、条件が増えたりすると可読性が悪化し、条件追加時にミスをする恐れがありますし、なにより美しくありません。

そこで、先日公開したこちらのアイテムに組み込んだスクリプトをベースにテーブルを用いてswith-case文を疑似的に実現する手法を説明します。

テーブルを使って、変数に値をセットする

前述のスクリプトをテーブルを用いて、書いた場合以下のようになります。

-- テーブルを使用し、キーと対応した値を書く
local CameraTbl = {
    ["カメラ1"] = "camera1", 
    ["Camera1"] = "camera1", 
    ["camera1"] = "camera1", 
    ["Cam1"] = "camera1", 
    ["cam1"] = "camera1", 

    ["カメラ2"] = "camera2", 
    ["Camera2"] = "camera2", 
    ["camera2"] = "camera2", 
    ["Cam2"] = "camera2", 
    ["cam2"] = "camera2", 
    -- カメラ3,4,5についても同様
}

function onMessage(sender, name, message)
    if name == "comment" then
        local str = CameraTbl[message]

        -- テーブルに存在しないキーを指定した場合、nilが返る(defaultの動作に相当)
        if str ~= nil then
            src = cameraModule.GetPosition()

            local item = vci.assets.GetSubItem(str)
            dst = item.GetPosition()
            start = vci.me.UnscaledTime
        end
    end
end


ほかの言語だとこんな感じ
String str

switch(message) {
    case :
    case "カメラ1" :
    case "Camera1" :
    case "camera1" :
    case "Cam1" :
    case "cam1" :
        str = "camera1";
        break;
    case "カメラ2" :
    case "Camera2" :
    case "camera2" :
    case "Cam2" :
    case "cam2" :
        str = "camera2";
        break;
    default :
       break;
}

function onMessage(sender, name, message)の関数内のはかなりすっきりしました。
テーブルを1つ用意しておけば、それを複数箇所から参照して動作する場合でも、テーブルのみを修正することになるため、ミスの可能性が減るかと思います。
また、テーブルについてはモジュールで別のソースに分離することも可能ですので、テーブルは別ソースに記述し、メインのソースからはアクセッサを通して参照することも可能です。

テーブルを使って、関数を呼び分ける

lua言語の変数は関数型をサポートしており、関数型の変数についてもテーブルに格納することが出来ます。
そのため、テーブルに関数型の値、テーブルを用いて引数テーブルを持つことでテーブルを用いて処理を分岐させることが可能です。

-- 使用可能なアイテム毎に関数型の値と引数のテーブルを持つ
local itemTbl = {
    ["eye"] = {["function"] = switchVisiable, ["args"] = nil }, 
    ["move"] = {["function"] = sendMsg, ["args"] = {"move"} }, 
    ["panel1"] = {["function"] = panelChange, ["args"] = {1} }, 
    ["panel2"] = {["function"] = panelChange, ["args"] = {2} }, 
    ["panel3"] = {["function"] = panelChange, ["args"] = {3} }, 
    ["panel4"] = {["function"] = panelChange, ["args"] = {4} }, 
    ["panel5"] = {["function"] = panelChange, ["args"] = {5} }, 
    ["xMinus"] = {["function"] = sendMsg, ["args"] = {"xMinus"} }, 
    ["yMinus"] = {["function"] = sendMsg, ["args"] = {"yMinus"} }, 
    ["zMinus"] = {["function"] = sendMsg, ["args"] = {"zMinus"} }, 
    ["xPlus"] = {["function"] = sendMsg, ["args"] = {"xPlus"} }, 
    ["yPlus"] = {["function"] = sendMsg, ["args"] = {"yPlus"} }, 
    ["zPlus"] = {["function"] = sendMsg, ["args"] = {"zPlus"} }, 
}

---[SubItemの所有権&Use状態]アイテムをグラッブしてグリップボタンを押すと呼ばれる。
---@param use string @押されたアイテムのSubItem名
function onUse(use)
    if itemTbl[use] then
        local func = itemTbl[use]["function"] 
        local args = itemTbl[use]["args"] 
        -- funcがnil(テーブルにキーがない)場合、使用しても何も起こらないアイテム
        if func ~= nil then
            if args == nil then
                func()
            elseif #args == 1 then
                func(args[1])
            end
        end
    end
end


テーブルを使わない場合だとこんな感じ
---[SubItemの所有権&Use状態]アイテムをグラッブしてグリップボタンを押すと呼ばれる。
---@param use string @押されたアイテムのSubItem名
function onUse(use)
    if use == "eye" then
        switchVisiable()
    elseif use == "panel1" or use == "panel2" or use == "panel3" or use == "panel4" or use == "panel5" then
        local args
        if use == "panel1" then
            args = 1
        elseif use == "panel2" then
            args = 2
        elseif use == "panel3" then
            args = 3
        elseif use == "panel4" then
            args = 4
        elseif use == "panel5" then
            args = 5
        end
        panelChange(args)
    elseif use == "move" or use == "xMinus" or use == "yMinus" or use == "zMinus" or use == "xPlus" or use == "yPlus" or use == "zPlus" then
        local args
        if use == "move" then
            args = "move"
        elseif use == "xMinus" then
            args = "xMinus"
        elseif use == "yMinus" then
            args = "yMinus"
        elseif use == "zMinus" then
            args = "zMinus"
        elseif use == "xPlus" then
            args = "xPlus"
        elseif use == "yPlus" then
            args = "yPlus"
        elseif use == "zPlus" then
            args = "zPlus"
        end
        sendMsg(args)
    end
end

複数の条件で呼び出す際のパラメータを変えつつ、呼び出す処理がかなり簡潔になりました。

最後に

今回の手法はpythonでswitch-case文を実現できないかと検索していた際に、こちらのページ(【Python入門】switch文の代わりに使える書き方)を見つけ、lua言語でも使用可能なtipsであると感じたのがきっかけです。
今回はコメントの内容やアイテム名を使用しての解説でしたが、時間をstring型に変換してタイムテーブルとして扱うなどluaのテーブル型は応用が利くため、色々トライしてみてください。

P.S.公開中のアイテムのスクリプトライセンスはMITですので、抜き出して改造を認めています。