New types and operators
An int can be a price, a distance or a user id, and the compiler cannot tell them apart. Salam lets you build new types so that code says what a value means, and so that mixing up two kinds of value becomes a compile error instead of a bug.
Aliases and distinct types
type Id = int is only another name for int and mixes freely with it. type Meter: int, on the other hand, builds a new type on top of int that cannot be confused with a plain int, and that can carry its own methods and operators in a body closed by end.
type Id = int
type Meter: int
func +(other: Meter): Meter:
ret ((this as int) + (other as int)) as Meter
end
func kilometers(): f64:
ret (this as f64) / 1000.0
end
end
func main:
id := 42 as Id
println id + 1
road := 1200 as Meter
println (road + road).kilometers()
endAn alias is useful for a long type you write often, or to give a name to an enum from another place: after type Paint = Color, Paint.Red is the same case as Color.Red. A type error that involves an alias names it too, as in cannot pass 'str' to 'i32' ('Age' is an alias of 'i32').
Distinct types for meaning
A distinct type keeps every operator of its base type, and the result has the new type: adding two Cents gives Cents, and < and > work as before. What it does not do is mix with plain numbers. price + 4 with 4 in an int variable stops with E021: operator cannot be applied to 'Cents' and 'i32'; a conversion with as has to be written out, so it is always visible. When an operation with another type makes sense, declare it, as * by a count does here:
type Cents: int
func *(count: int): Cents:
ret ((this as int) * count) as Cents
end
func text(): str:
c := this as int
rest := c % 100
pad := rest < 10 ? "0" : ""
ret "$" + (c / 100) + "." + pad + rest
end
end
func main:
price := 499 as Cents
shipping := 250 as Cents
total := price * 3 + shipping
println total.text(), total > price
println total
people := 2
share := ((total as int) / people) as Cents
println "each pays", share.text()
endprintln total prints the underlying number, so the example asks for the formatted text with its own text() method. A number literal adopts the type it meets, so price + 100 needs no cast, while price * 3 uses the * declared in the type.
The base can also be str, bool, char or a float. An Email is still text, but a function that takes one can no longer be handed any string by mistake (E012: argument 1: cannot pass 'str' to 'Email'):
type Email: str
func domain(): str:
s := this as str
at := s.find("@")
ret s.substr(at + 1, s.len() - at - 1)
end
end
func notify(recipient: Email, subject: str):
println "to", recipient as str, "at", recipient.domain(), "about", subject
end
func main:
user := "sara@example.com" as Email
notify(user, "your order")
endOperator overloading
A method whose name is the operator itself, such as func +(other: Vec): Vec, defines that operator for the struct. It is called when the struct is the left operand. The operators you can define are + - * / % ** == != < > <= >= not [] []=; inventing a new symbol is an error.
struct Vec:
pub x: int = 0
pub y: int = 0
pub func +(other: Vec): Vec:
ret Vec { x = this.x + other.x, y = this.y + other.y }
end
pub func ==(other: Vec): bool:
ret this.x == other.x and this.y == other.y
end
end
func main:
a := Vec { x = 1, y = 2 }
b := Vec { x = 3, y = 4 }
println a + b
println a == b, (a + b) == Vec { x = 4, y = 6 }
endIndexing, negation and the rest
The number of parameters decides the kind of operator. func -: Vec with no parameters is negation (-pos), and func -(other: Vec) would be subtraction; define both if you need both. [] takes the index and []= takes the index and the new value, which lets a struct be used with square brackets like an array:
struct Board:
cells: int[9] = [0, 0, 0, 0, 0, 0, 0, 0, 0]
pub func [](i: int): int:
ret this.cells[i]
end
pub mut func []=(i: int, mark: int):
this.cells[i] = mark
end
end
struct Vec:
pub x: int = 0
pub y: int = 0
pub func +(other: Vec): Vec:
ret Vec { x = this.x + other.x, y = this.y + other.y }
end
pub func *(k: int): Vec:
ret Vec { x = this.x * k, y = this.y * k }
end
pub func -: Vec:
ret Vec { x = -this.x, y = -this.y }
end
pub func ==(other: Vec): bool:
ret this.x == other.x and this.y == other.y
end
end
func main:
mut board := Board {}
board[4] = 1
board[0] = 2
println board[4], board[0], board[8]
mut pos := Vec { x = 1, y = 2 }
pos += Vec { x = 10, y = 0 }
println pos, pos * 3, -pos
println pos != Vec {}
endSome operators come for free. a += b uses your +, and != uses your == when you have not written a !=. The right operand of * does not have to be the same type: here a Vec is scaled by an int.
Define an operator only when its meaning is obvious to a reader. + on two vectors is clear; + that saves a record to disk is not, and a method with a name is the better choice.
Variant
Variant<int, str> is a value that may be any one of several types. Match on the type names to get the value back with its real type. The match has to cover every type in the list.
func describe(v: Variant<int, str>): str:
ret match v:
int n: "number " + n end
str s: "text " + s end
end
end
func main:
mut value := 21 as Variant<int, str>
println describe(value)
value = "off"
println describe(value)
endA Variant is often a field: a setting whose value may be a number, a switch or a name. Assign a value of one of the listed types directly, without a cast:
struct Setting:
pub key: str = ""
pub value: Variant<int, bool, str> = 0
end
func show(s: Setting): str:
ret s.key + " = " + match s.value:
int n: "" + n end
bool flag: flag ? "yes" : "no" end
str text: "\"" + text + "\"" end
end
end
func main:
mut settings := Vector {} as Vector<Setting>
defer settings.free()
settings.push(Setting { key = "port", value = 8080 })
settings.push(Setting { key = "debug", value = true })
settings.push(Setting { key = "name", value = "shop" })
each s in settings:
println show(s)
end
endA Variant cannot be printed as it is (E002: cannot print value of type 'Variant<i32, str>' directly); match on it and print the part you got, as show does. When the choices need names of their own or carry several values, an enum with data is usually the clearer tool.
Which one to use
| You want | Use |
|---|---|
| A shorter or clearer name for an existing type | type Name = T |
| A number or string that must not be mixed up with others | type Name: T ... end |
| Several named fields together | a struct |
| One of a fixed list of cases, perhaps with data | an enum |
| A value of one of a few existing types | Variant<A, B> |
| One function or struct that works for many types | generics |
Rules of thumb
- Give a distinct type to values that are easy to confuse: money, units, ids, already-escaped text.
- Convert in and out with
asat the edges of the program, and keep the distinct type everywhere in between. - Overload operators only where they read naturally, and keep them free of side effects.
- Prefer an enum with data to a Variant when the cases have meanings beyond their types.