Step 10 of 14, updated

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.

An 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:

struck ShotScript writes it for youhighlighted new in ShotScript
Luau todayCoins (a ModuleScript, part)
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
ShotScriptWallet/Coins.shot (part)
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
  • match takes a union of strings. Each case starts its line with one or more values, then then; its code runs up to the next case.
  • An else, if you write one, goes last and takes every value without a case. end closes the match.
  • It compiles to a plain if / elseif, line for line, so errors keep their .shot lines 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) raises match: no case for <value>.

Run it

Wallet gets a method that turns its history into lines. It goes inside the class, just above Describe:

ShotScriptWallet/Wallet.shot (part)
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
LuauScenarios/History.luau
-- 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
$ shot run History
==== 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:

$ shot check
./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:

$ shot matches Reason
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.

Try it

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.