Expressive Control Flow
Luwu adds a few control flow tools that Luau doesn't have to improve expressiveness and reduce visual overhead.
if local
In Luau, you often get a value, then check that it exists:
const scores: { [string]: number? } = { ada = 92 }
local score = scores["ada"]
if score then
print(`Ada scored {score}`)
endWith if local, you bind the value inside the condition:
const scores: { [string]: number? } = { ada = 92 }
if local score = scores["ada"] then
print(`Ada scored {score}`)
endThe branch runs when the value is truthy: not nil, false or none.
score only exists inside the branch and can't leak into the code after it.
Use if const when the branch won't reassign the value. Most of the time, that's what you want.
type User = { name: string }
const users: { User } = { { name = "Ada" }, { name = "Grace" } }
const function find(name: string): User?
for _, user in users do
if user.name == name then
return user
end
end
return nil
end
if const user = find("Grace") then
print(`Found {user.name}`)
elseif const fallback = find("Ada") then
print(`Using {fallback.name} instead`)
else
print("Nobody here")
endwhen chains
Add more checks with when. Each clause must pass for the branch to run.
A clause can be a plain condition or a binding that later clauses can use.
const players: { [string]: { level: number, guild: string? } } = {
ada = { level = 12, guild = "Builders" },
grace = { level = 3 },
}
const function invite(id: string)
if
const player = players[id]
when player.level >= 10
when const guild = player.guild
then
print(`{id} can lead raids for {guild}`)
else
print(`{id} can't lead raids yet`)
end
end
invite("ada")
invite("grace")The chain checks its clauses left to right and stops at the first one that fails.
A binding clause can destructure. Every name it binds must be truthy.
const function parse_point(text: string): { x: number?, y: number? }
const x, y = string.match(text, "^(%-?%d+),(%-?%d+)$")
return { x = tonumber(x), y = tonumber(y) }
end
if const .{ x, y } = parse_point("3,4") then
print(x + y)
endif const in expressions
if local and if const also work in if-else expressions.
const nicknames: { [string]: string? } = { grace = "Amazing Grace" }
const function display_name(id: string): string
return if const nickname = nicknames[id] then nickname else id
end
print(display_name("grace"), display_name("ada"))when in loops
Luwu's loops take when chains too.
while local
while local binds a fresh value on every pass. The loop ends as soon as the chain fails.
const jobs = { "build", "test", "deploy" }
while const job = table.remove(jobs, 1) do
print(`Running {job}`)
endAdd a when clause to stop early:
const lines = { "name = Ada", "level = 12", "", "ignored = true" }
local index = 0
while const line = lines[index + 1] when line ~= "" do
index += 1
print(line)
endfor ... when
Instead of wrapping the whole loop body in an if, put a when chain after a for loop's header to skip the items that fail it.
const inventory = {
{ name = "Arrow", count = 24 },
{ name = "Potion", count = 0 },
{ name = "Rope", count = 2 },
}
for _, item in inventory when item.count > 0 do
print(`{item.name} x{item.count}`)
endBindings work here too, and so do numeric loops:
const prices: { [string]: number? } = { apple = 1.25, pear = 0.8 }
const basket = { "apple", "kiwi", "pear" }
for _, fruit in basket when const price = prices[fruit] do
print(`{fruit} costs {price}`)
end
for n = 1, 20 when n % 3 == 0 and n % 2 == 1 do
print(n)
enddo expressions
Sometimes one value takes several statements to work out. A do expression runs a block and gives back a value.
give ends the block and produces its value. There's no end.
In Luwu we prefer do/give blocks over temporary and intermediate variables. Scoping the computation inside the expression that produces a value makes it obvious what the code is building up. A do expression also fits several statements where only an expression goes, like do print(x) give x to debug a value in place. Unlike a function call that abstracts the same computation, it's always inlined and costs nothing.
In Luau, the temporaries sit next to the result and stay in scope for the rest of the function:
const price, quantity = 4.5, 12
local subtotal = price * quantity
local discount = if quantity >= 10 then subtotal * 0.1 else 0
local total = subtotal - discount
print(total)With a do expression, subtotal and discount live inside the block that computes total. Readers see at a glance that they exist only for it:
const price, quantity = 4.5, 12
const total = do
const subtotal = price * quantity
const discount = if quantity >= 10 then subtotal * 0.1 else 0
give subtotal - discount
print(total)A loop that builds up one value works the same way:
const words = { "luwu", "classes", "traits" }
const longest = do
local best = ""
for _, word in words do
if #word > #best then
best = word
end
end
give best
print(longest)Because the temporary best stays inside the block, longest can be const.
A give can also leave the block early, like a return. This one stops at the first player who is online and only falls back to the last line when nobody is:
const players = {
{ name = "Ada", online = false },
{ name = "Grace", online = true },
{ name = "Alan", online = true },
}
const host = do
for _, player in players do
if player.online then
give player.name
end
end
give "nobody"
print(host)or return, or continue, or break
With or return, a guard clause that checks for a missing value and leaves early fits on one line.
const prices: { [string]: number? } = { apple = 1.25, pear = 0.8 }
const function total(order: { [string]: number? }): number?
local sum = 0
for item, count in order do
const price = prices[item] or return nil
sum += price * count
end
return sum
end
print(total({ apple = 4 }))
print(total({ apple = 1, kiwi = 2 }))After the guard, Luwu knows price isn't nil.
or continue and or break work the same way inside loops:
const entries = { "hp=10", "broken", "mp=4", "stop", "xp=99" }
for _, entry in entries do
if entry == "stop" then
break
end
const key, value = string.match(entry, "^(%a+)=(%d+)$")
if not key or not value then
continue
end
print(key, value)
end
for _, entry in entries do
entry ~= "stop" or break
const key, value = string.match(entry, "^(%a+)=(%d+)$")
key and value or continue
print(key, value)
endBoth loops print the same thing. The second one reads top to bottom with every exit at the left edge.