Lesson 16 of 25 · Data and types

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-alias.salam
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()
end
Output43 2.4

An 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-money.salam
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()
end
Output$17.47 true 1747 each pays $8.73

println 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.salam
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")
end
Outputto sara@example.com at example.com about your order

Operator 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.

operator.salam
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 }
end
OutputVec {x = 4, y = 6} false true

Indexing, 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:

operator-more.salam
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 {}
end
Output1 2 0 Vec {x = 11, y = 2} Vec {x = 33, y = 6} Vec {x = -11, y = -2} true

Some 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.

variant.salam
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)
end
Outputnumber 21 text off

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

variant-settings.salam
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
end
Outputport = 8080 debug = yes name = "shop"

A 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 wantUse
A shorter or clearer name for an existing typetype Name = T
A number or string that must not be mixed up with otherstype Name: T ... end
Several named fields togethera struct
One of a fixed list of cases, perhaps with dataan enum
A value of one of a few existing typesVariant<A, B>
One function or struct that works for many typesgenerics

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 as at 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.