SCUA 0.30.0 adds r matches Ok and
r matches Error, which answer true or
false for whether a Result succeeded. It also makes
as(value, Record) the way to turn data into a record, and a
record now keeps its type wherever it goes: to an actor, into a save and
back, or across a cluster. A few older spellings stop working, and the
last section lists each one with what to write now.
#matches Ok tests a
Result
A function that can fail returns Ok or
Error. When you only need to know which one came back, ask
with matches:
import json
fn parse_port(text)
let n = tonumber(text)
if n == nil or n < 1 or n > 65535 then return Error(`not a port: {text}`) end
return Ok(n)
end
for text in ["8080", "http"] do
let r = parse_port(text)
if r matches Ok then
print(`{text}: listening`)
end
if r matches Error then
print(`{text}: using the default`)
end
end
print(json.decode("[1, 2") matches Error)
print(nil matches Ok)
8080: listening
http: using the default
true
false
Both give true or false and work on any
value, so nil matches Ok is just false. When
you want what is inside the Result as well, match is still
the way to take it apart.
A Result used directly as a condition stops with an error that shows
both forms. Where the compiler can see that a function always returns
Ok or Error, it says so before the program
runs. With the same parse_port:
if parse_port("http") then
print("listening")
end
scua: condition.scua:7: type error: a condition got a Result (`parse_port` always returns Ok or Error): an Ok/Error is never true or false. Handle both cases:
match r Ok(v) -> … Error(e) -> … end
Or test it: `r matches Ok` for success, `r matches Error` for failure.
A flags set works the same way. Ask whether it holds a
member with in, as in Perm.Write in perms, and
whether it holds any of several with
perms & (Perm.Read | Perm.Write) != Perm.none.
Everything else in a condition behaves as before: 0,
"" and empty collections are true, and only
nil and false are false.
#as turns data into a
record
Decoded JSON, a loaded save and a table built by hand are all plain
data. as(value, Record) makes a record from it. It copies
the fields, fills in any defaults the data leaves out, runs the record's
migrate hook for data in an older shape, and checks the
result, at every level of nesting:
import json
record Player { name: string, level: int = 1, gold: int = 0 }
migrate Player(old)
return { name = old.nick ?? old.name, level = old.level ?? 1, gold = old.coins ?? old.gold ?? 0 }
end
fn describe(p: Player) -> string
return `{p.name}, level {p.level}, {p.gold} gold`
end
let saves = [
"{\"name\": \"Ana\", \"level\": 7, \"gold\": 120}",
"{\"name\": \"Bo\"}",
"{\"nick\": \"Cy\", \"coins\": 40}",
]
for text in saves do
match json.decode(text)
Ok(data) -> print(as(data, Player).describe())
Error(e) -> print(`not JSON: {e}`)
end
end
Ana, level 7, 120 gold
Bo, level 1, 0 gold
Cy, level 1, 40 gold
The second save has no level or gold and gets the defaults. The third
is from before nick became name and
coins became gold, and the hook reads it. What
comes back is a Player every time, so its methods work on
it.
A type written on a let, a parameter or a return now
only checks a value, and leaves it as it was. So when you see
as(...), you know data is being turned into a record at
that point. A literal written right where the type is declared still
builds the record, as before:
let p: Point = { x = 1, y = 2 } is a Point,
and so is a literal returned from a function declared
-> Point.
#Records keep their type wherever they go
A record you send to an actor arrives as that record, with its
methods. The same holds when it is nested inside another value, returned
from an ask, or kept in an actor's state while the actor is
frozen with actors.freeze and brought back with
actors.revive:
import actors
record Line { item: string, cents: int, qty: int = 1 }
record Order { id: string, lines: { Line } }
fn cost(l: Line) -> int return l.cents * l.qty end
fn cost(o: Order) -> int
let sum = 0
for l in o.lines do sum = sum + l.cost() end
return sum
end
partition Till
state last = nil
on Take(o)
last = o
print(`order {o.id}: {o.cost()} cents`)
end
on Show() print(`after a revive, order {last.id} is still an Order: {last.cost()} cents`) end
end
let till = Till()
tell till.Take(as({ id = "A-17", lines = [{ item = "tea", cents = 250, qty = 2 }, { item = "cake", cents = 320 }] }, Order))
wait(100ms)
fn restart(a)
let blob = actors.freeze(a)?
actors.drop(a)
return actors.revive(blob)
end
match restart(till)
Ok(revived) -> tell revived.Show()
Error(e) -> print(`revive refused: {e.message}`)
end
order A-17: 820 cents
after a revive, order A-17 is still an Order: 820 cents
There are two functions called cost here, one for a
Line and one for an Order. The actor picks the
right one because each value it holds still knows which record it is,
including the lines nested inside the order.
A revive checks each record type the saved actor holds, by name. If
you change the fields of Order and then revive an actor
that holds one, the refusal names Order and lists the saved
fields beside the current ones. Changing a record type the actor does
not hold has no effect on the revive. Before you deploy a change to a
record, scua blob-info shows which record types a save
holds, with their fields.
A shared cluster table keeps records too. Here one node stores two records, and a second node on the same machine reads them back and calls the method for each:
import cluster
import sys
record Price { item: string, cents: int }
fn label(p: Price) -> string return `{p.item} at {p.cents} cents` end
record Stock { item: string, count: int }
fn label(s: Stock) -> string return `{s.count} {s.item}s in stock` end
let me = tonumber(sys.args()[0])
let peer = tonumber(sys.args()[1])
let cfg = cluster.open("prices")
cluster.join({ table = "prices", bind = "127.0.0.1", port = me, seeds = [`127.0.0.1:{peer}`] })
if me < peer then
cluster.set(cfg, "price/apple", as({ item = "apple", cents = 30 }, Price))
cluster.set(cfg, "stock/apple", as({ item = "apple", count = 12 }, Stock))
wait(3s)
else
for i in 0:50 do
if cluster.has(cfg, "stock/apple") then break end
wait(100ms)
end
print(cluster.get(cfg, "price/apple").label())
print(cluster.get(cfg, "stock/apple").label())
end
$ scua --allow-cluster nodes.scua 17966 17967 &
$ scua --allow-cluster nodes.scua 17967 17966
apple at 30 cents
12 apples in stock
A node whose program declares the record with different fields refuses the value and names the record. During a rolling deploy that changes a stored record, write the new shape under a new key until every node runs the new program.
#What changes in your code
Most of these stop with an error that names the fix. The third and the last change what a program does without an error, so they are the ones to search your code for.
- A Result or a
flagsset is no longer accepted as a condition, inif,while,assert,not,and/or, amatchguard or awhererule. Writer matches Ok, take it apart withmatch, or test a set withPerm.Write in perms. For a default when a value is missing, use??. OkandErrorare reserved. A record, enum variant, contract or message with either name is an error. Rename it, for example toParseError.- A typed
let, parameter, return or field no longer fills a missing default or runsmigrate, so a defaulted field the data leaves out readsnil. Where you loaded a save or decoded JSON through a typedlet, addas:let hero = as(save, Player). A literal passed to a record-typed parameter is checked, not built, so writeheal(as({ name = "Ed", hp = 10 }, Player))when the function needs the defaults. - A record you receive in a message is sealed like one you built yourself, so adding a field it does not declare is an error. Copy the fields you want into a table of your own, or declare the field.
- A
recordgoes at the top level of a file. Declared inside a function or a block, it is now an error. x matches Cis alwaystrueorfalse. Code that used the value of the last clause should use that expression directly.
The changelog has every change in
0.30.0. Testing a
Result and Records and
types in the guide cover the new forms, and
examples/results-and-flags.scua and
examples/record-messages.scua in the distribution run
them.