Match
match runs code by a string union's value. Add a value to the union, and shot check points at every match that doesn't handle it yet.
You'll learn match, its cases and else, and how a new value becomes a to-do list.
match entry.Reasona union of strings"Pickup" thenhas a case"Shop" thenhas a case"Daily"a new value with no case: shot check names itAn if chain that knows the union
Each change has a Reason, "Pickup" | "Shop". Instead of an if chain and an error for the value nobody expected:
function Coins.Line(entry: Entry): string local n = math.abs(entry.Amount) local s = Coins.Format(n) if entry.Reason == "Pickup" then return "Picked up " .. s elseif entry.Reason == "Shop" then return "Spent " .. s end error("no case for " .. entry.Reason)end
doc { One wallet change as a line of text }function Line(entry : Entry) : string local n = math.abs(entry.Amount) local s = Coins.Format(n) match entry.Reason "Pickup" then return "Picked up " .. s "Shop" then return "Spent " .. s endend
What it compiles toWallet/Coins.luau (part)
function Coins.Line(entry : Entry) : string local n = math.abs(entry.Amount) local s = Coins.Format(n) do local __shotmatch1 : typeof(entry.Reason) = entry.Reason if __shotmatch1 == "Pickup" then return "Picked up " .. s elseif __shotmatch1 == "Shop" then return "Spent " .. s else local __shotmatch_5 : nil = __shotmatch1 error("match: no case for " .. tostring(__shotmatch1)) end end end
matchtakes a union of strings. Each case starts its line with one or more values, thenthen; its code runs up to the next case.- An
else, if you write one, goes last and takes every value without a case.endcloses the match. - It compiles to a plain
if/elseif, line for line, so errors keep their.shotlines and the value is read once. - In the compiled
else, Luau's checker knows which values are left and names them. At run time a value no case covers (only with wrong data) raisesmatch: no case for <value>.
Run it
Wallet gets a method that turns its history into lines. It goes inside the class, just above Describe:
doc { Every change so far, as text } public Lines() : {string} local lines = {} for _, entry in self._history do table.insert(lines, Coins.Line(entry)) end return lines end
-- History: a pickup and a purchase, read back as lines of text. return function(world) local Bank = world.Server.Require("Wallet.Bank") world:Boot() local ana, client = world:AddPlayer("Ana") world:Step(0.1) Bank.Give(ana, 25) client.Require("Wallet.Shop").Client.Buy("Sword") world:Step(0.1) for _, line in Bank.Wallets[ana]:Lines() do world.Log(line) end end
==== History [client:Ana print] client started Picked up 25 coins Spent 10 coins ==== summary History OK (3 lines, 0 errors, 0.2s virtual) 1 of 1 scenario(s) passed
A new value is a to-do list
Add a daily reward, public type Reason = "Pickup" | "Shop" | "Daily", and change nothing else. An if chain would fail at the first daily reward; the match is an error at once:
./Wallet/Coins.shot:14:3 match on Reason has no case for "Daily" (added at Wallet/Types.shot:2): add the case, or an else 1 errors in 1 file (16 files checked, 2.8 s)
shot matches lists every match on a union and what each one leaves out:
matches on Reason = "Pickup" | "Shop" | "Daily" (Wallet/Types.shot:2) Wallet/Coins.shot:14 match entry.Reason MISSING "Daily"
Tip Prefer a match without else, so a new value shows up everywhere it needs handling. For numbers, enums and other types, write if / elseif.
Add "Daily" to Reason, then add the case "Daily" then return "Daily reward: " .. s to the match, and run shot matches Reason again: it says every value.
