Enums
An enum is a type with a closed set of cases: a traffic light is red, green or yellow and nothing else. Because the compiler knows every case, it can tell you when your code forgets one. A case can also carry data of its own, which makes enums the natural way to say "one of these kinds, with these details".
A plain enum
Cases are separated by commas and numbered from zero. You can give a case a value of your own, and the cases after it carry on from there. as int gives you the number behind a case.
enum Status:
Draft, Published = 10, Archived
end
func main:
state := Status.Published
println state as int, Status.Archived as int
println state == Status.Published
end.name() gives the name of a case as a string, and .Count() on the enum itself gives the number of cases:
enum Season: Spring, Summer, Autumn, Winter end
func main:
s := Season.Autumn
println s.name()
println "number of seasons:", Season.Count()
println s == Season.Autumn, s as int
endEnums and match
match on an enum lists the cases by their bare names. It must cover every case or end with else; leave one out and the program does not compile: E080: non-exhaustive match on enum 'Light': missing member Yellow. That check is the main reason to use an enum instead of plain numbers or strings. When you add a case later, the compiler points at every match that has to learn about it.
enum Light: Red, Green, Yellow end
func next(l: Light): Light:
ret match l:
Red: Light.Green end
Green: Light.Yellow end
Yellow: Light.Red end
end
end
func seconds(l: Light): int:
ret match l:
Red: 30 end
Green: 25 end
Yellow: 5 end
end
end
func main:
mut light := Light.Red
mut cycle := 0
repeat Light.Count():
println light.name(), seconds(light)
cycle += seconds(light)
light = next(light)
end
println "full cycle:", cycle, "seconds, back to", light.name()
endEach function describes one fact about every state, and Light.Count() makes the loop go round exactly once, however many cases there are. A small state machine like this, where each state knows the next one, turns up everywhere: order processing, game turns, connection status.
Write else only when the remaining cases really share one answer. A catch-all else also swallows the cases you add later, and the compiler can no longer remind you about them.
Enums in structs and stored values
An enum is a good type for a struct field with a fixed set of states. Printing the struct shows the field as its number, so use name() when a person will read it.
enum Status: Placed, Packed, Shipped, Delivered end
struct Order:
pub id: int = 0
pub status: Status = Status.Placed
end
func status_from(code: int): Status:
if code < 0 or code >= Status.Count():
ret Status.Placed
end
ret code as Status
end
func main:
mut order := Order { id = 1042 }
order.status = Status.Packed
println order
println order.id, order.status.name()
saved := order.status as int
println "saved as", saved, "read back as", status_from(saved).name()
println "unchecked:", "[" + (9 as Status).name() + "]", "checked:", status_from(9).name()
endThe number is what you store in a file or a database, and code as Status turns it back into a case. That conversion is not checked: 9 as Status is accepted even though no case has the value 9, and its name() is an empty string. When the number comes from outside the program, check it first, as status_from does with Status.Count(). When the cases have values of their own, such as Published = 10, compare against those instead.
Enums that carry data
A case can carry named data, such as a radius for a circle. Build one with Shape.Circle(2.0), and use match to tell the cases apart and pull the data out. A branch may add a condition with if. A guarded branch does not count as covering its case, so keep an unguarded branch for the same case after it.
enum Shape:
Circle(radius: f64)
Rectangle(width: f64, height: f64)
None
end
func area(s: Shape): f64:
ret match s:
Circle(r): 3.14 * r * r end
Rectangle(w, h) if w == h: w * w end
Rectangle(w, h): w * h end
None: 0.0 end
end
end
func main:
println area(Shape.Circle(2.0))
println area(Shape.Rectangle(3.0, 4.0))
println area(Shape.None)
println Shape.Circle(1.5)
endIn a pattern, _ skips a value you do not need, and Voucher v binds the whole case to one name, so you can read its fields as v.code. Cases with data compare with == on all of their values, and name() and Count() work as they do on a plain enum:
enum Payment:
Cash
Card(last4: str, amount: int)
Voucher(code: str)
end
func label(p: Payment): str:
ret match p:
Cash: "cash" end
Card(digits, _): "card ending " + digits end
Voucher v: "voucher " + v.code end
end
end
func main:
payments := [Payment.Cash, Payment.Card("4242", 120), Payment.Voucher("SPRING")]
each p in payments:
println p.name(), "->", label(p)
end
println Payment.Card("4242", 120) == payments[1], payments[0] == payments[2]
println "kinds of payment:", Payment.Count()
endAn enum with data needs at least two cases, and its cases cannot also have = value.
Generic enums
An enum that carries data can be generic too. Maybe<T> either holds a value or holds nothing: a safe way to say "there is no result" without a null.
enum Maybe<T>:
Some(value: T)
None
end
func divide(a: int, b: int): Maybe<int>:
if b == 0:
ret Maybe.None
end
ret Maybe.Some(a / b)
end
func describe(m: Maybe<int>): str:
ret match m:
Some(v): "the result is " + v end
None: "division by zero" end
end
end
func main:
println describe(divide(10, 2))
println describe(divide(1, 0))
endType parameters themselves are covered in generics and interfaces.
Result and the ? operator
Salam has no exceptions. The result package provides Result<T, E>, which is either Ok with a value or Err with an error. Inside a function that returns a Result, writing ? after a call unwraps the successful value, and on an error returns that error straight away.
import result
func number(text: str): result.Result<int, str>:
if text == "7":
ret result.Result.Ok(7)
end
ret result.Result.Err("not a valid number: " + text)
end
func add(a: str, b: str): result.Result<int, str>:
x := number(a)?
y := number(b)?
ret result.Result.Ok(x + y)
end
func main:
println add("7", "7")
println add("7", "seven")
endThe ? has to be the whole value of its statement: x := f()?, x = f()?, f()? or ret f()?. A defer in the function still runs when ? returns early.
Working with a Result
Where the error does not need to travel further, the package has small helpers. result.IsOk and result.IsErr tell you which case you have, result.UnwrapOr(r, fallback) gives the value or a fallback, and result.Expect(r, message) gives the value or stops the program with the message. A Result is an ordinary enum, so match works on it too.
import result
func parse_age(text: str): result.Result<int, str>:
n := text.to_int()
if n <= 0 or n > 150:
ret result.Result.Err("not an age: " + text)
end
ret result.Result.Ok(n)
end
func main:
good := parse_age("42")
bad := parse_age("abc")
println result.IsOk(good), result.IsErr(bad)
println result.UnwrapOr(bad, 18)
println result.Expect(good, "an age is required")
println match bad:
Ok(age): "age " + age end
Err(msg): "error: " + msg end
end
endKeep Expect for errors that mean a bug in the program, not for bad input from a user: when it fails, the program ends with salam panic: followed by your message.
Rules of thumb
- Use an enum whenever a value is one of a known, fixed list. A string can be misspelled or written in the wrong case (
"shipped"where the code expects"Shipped") without anyone noticing, while a wrong enum case does not compile. - Prefer
matchwithoutelseon your own enums, so the compiler finds every place a new case must be handled. - Check numbers that come from outside the program before converting them with
as. - Give each case the data that belongs to it, instead of one struct with fields that are only sometimes used.
- Return a
Resultfrom functions that can fail, and pass errors up with?.