Lesson 14 of 25 · Data and types

Structs

A struct gathers several related pieces of data under one name, along with the functions, called methods, that work on them. It is how a Salam program describes the things it deals with: an account, an order, a point on a map.

Declaring a struct

Every field has a name, a type and, optionally, a default value. A method is a func written inside the struct, and inside it this refers to the value the method was called on. Fields and methods are private by default; pub makes them reachable from outside the struct.

structs.salam
struct Rectangle:
    pub width: int = 1
    pub height: int = 1
    pub func area(): int:
        ret this.width * this.height
    end
end

func main:
    plot := Rectangle { width = 12, height = 8 }
    println "area of the plot:", plot.area()
end
Outputarea of the plot: 96

Creating and printing

A struct literal names the fields it sets: Point { x = 3, y = 4 }. Any field you leave out takes its default value. A field declared without a default has to be given every time, or the compiler stops with E028: struct literal 'P' requires field 'x'. A struct can be printed directly, with all of its fields:

struct-print.salam
struct Point:
    pub x: int = 0
    pub y: int = 0
end

func main:
    p := Point { x = 3, y = 4 }
    println p
    println Point {}
end
OutputPoint {x = 3, y = 4} Point {x = 0, y = 0}

Your own text with to_str

The printed form shows every field, which is handy while you write a program but not always what a user should see. Give the struct a method pub func to_str(): str and println uses it instead. It is also used when the struct sits inside another struct, a Vector or an array.

struct-tostr.salam
struct Money:
    pub cents: int = 0
    pub func to_str(): str:
        rest := this.cents % 100
        pad := rest < 10 ? "0" : ""
        ret "$" + (this.cents / 100) + "." + pad + rest
    end
end

struct Item:
    pub name: str = ""
    pub price: Money = Money {}
end

func main:
    pen := Item { name = "pen", price = Money { cents = 250 } }
    println pen.price
    println pen
    println "total: " + Money { cents = 1205 }.to_str()
end
Output$2.50 Item {name = "pen", price = $2.50} total: $12.05

Item has no to_str of its own, so it is printed field by field, but its price field uses the one from Money. To get the text without printing it, call to_str() yourself, as the last line does.

Public and private

A private field can only be read and written by the struct's own methods. Code outside that tries acc.balance gets E017: field 'balance' is private in struct 'Account' (mark it 'pub'). That is how a struct protects its rules: here nobody can make the balance negative, because the only way to change it is through deposit and withdraw, and both check the amount first.

struct-private.salam
struct Account:
    owner: str = ""
    balance: int = 0
    pub static func open(owner: str): Account:
        ret Account { owner = owner }
    end
    pub func owner_name(): str:
        ret this.owner
    end
    pub func current(): int:
        ret this.balance
    end
    pub mut func deposit(amount: int): bool:
        if amount <= 0:
            ret false
        end
        this.balance += amount
        ret true
    end
    pub mut func withdraw(amount: int): bool:
        if amount <= 0 or amount > this.balance:
            ret false
        end
        this.balance -= amount
        ret true
    end
end

func main:
    mut acc := Account.open("Sara")
    println acc.deposit(500), acc.withdraw(800), acc.withdraw(120)
    println acc.owner_name(), acc.current()
end
Outputtrue false true Sara 380

A good habit is to make fields private unless there is a reason to expose them, and to give the outside world methods that say what can be done (deposit) rather than how the data is stored (balance).

A method cannot have the same name as a field of its struct. That is why the method that returns the balance is called current and not balance.

Mutating methods

A method that changes the struct's fields is declared mut func. As soon as a struct has one, the compiler checks every method: a plain func that assigns to a field of this is an error (E105: cannot modify 'this' in method 'reset': declare it 'mut func'), and a mut func can only be called on a variable declared with mut (E106). So by reading a method's first line you know whether it can change the value.

struct-mut.salam
struct Counter:
    value: int = 0
    pub func read(): int:
        ret this.value
    end
    pub mut func increment():
        this.value += 1
    end
end

func main:
    mut c := Counter {}
    c.increment()
    c.increment()
    println c.read()
end
Output2

Static members and constructors

static func and const inside a struct belong to the type rather than to one value; call them through the struct's name. A static func has no this, but it can set private fields, which makes it the natural place for constructors. Account.open in the example above is one; here is another that builds a ready-made value:

struct-static.salam
struct Color:
    pub red: int = 0
    pub green: int = 0
    pub blue: int = 0
    pub const MAX := 255
    pub static func gray(value: int): Color:
        ret Color { red = value, green = value, blue = value }
    end
end

func main:
    white := Color.gray(Color.MAX)
    println white
end
OutputColor {red = 255, green = 255, blue = 255}

Structs are values

Assigning a struct copies it. After goal := start the two are separate values, and changing goal leaves start alone. Passing a struct to a function copies it too, so a function that should change the caller's struct takes it with &:. Two structs compare with == and != field by field, with no code of your own.

struct-copy.salam
struct Point:
    pub x: int = 0
    pub y: int = 0
end

func reset_copy(p: Point):
    p.x = 0
end

func reset(p &: Point):
    p.x = 0
end

func main:
    start := Point { x = 3, y = 4 }
    mut goal := start
    goal.y = 10
    println start, goal
    println start == Point { x = 3, y = 4 }, start == goal

    reset_copy(goal)
    println goal.x
    reset(goal)
    println goal.x
end
OutputPoint {x = 3, y = 4} Point {x = 3, y = 10} true false 3 0

reset_copy changed its own copy, which was thrown away when it returned. reset took a reference and changed the caller's goal. A &: parameter is also cheaper for a large struct, because nothing is copied. One exception to "a struct is a value": a Vector or a HashMap field is copied as a handle, so both copies share the same elements (see sharing and copying).

Composition with use

use places one struct inside another and makes its public fields and methods reachable directly. That is composition instead of inheritance: a Dog is built from an Animal plus a breed.

struct-embed.salam
struct Animal:
    pub name: str = ""
    pub func introduce(): str:
        ret "I am " + this.name
    end
end

struct Dog:
    pub use Animal
    pub breed: str = ""
end

func main:
    d := Dog { name = "Rex", breed = "hound" }
    println d.introduce()
    println d.name, d.breed
end
OutputI am Rex Rex hound

The embedded struct is also a field of its own, named after its type. If the outer struct defines a method with the same name as an embedded one, its own method wins, and it can still call the embedded version through that field:

struct-embed-override.salam
struct Timestamps:
    pub created: str = ""
    pub updated: str = ""
    pub func describe(): str:
        ret "created " + this.created + ", updated " + this.updated
    end
end

struct Post:
    pub use Timestamps
    pub title: str = ""
    pub func describe(): str:
        ret this.title + " (" + this.Timestamps.describe() + ")"
    end
end

func main:
    mut post := Post { title = "Hello", created = "2026-10-01" }
    post.updated = "2026-10-05"
    println post.describe()
    println post.created, post.Timestamps.updated
    println post
end
OutputHello (created 2026-10-01, updated 2026-10-05) 2026-10-01 2026-10-05 Post {Timestamps = Timestamps {created = "2026-10-01", updated = "2026-10-05"}, title = "Hello"}

A literal can set embedded fields directly (created = ...), and printing shows the embedded struct as a nested value. pub use makes the embedded struct visible outside; a plain use keeps it private.

Where to go next

Rules of thumb

  • Give fields sensible defaults, so a literal only names what differs.
  • Keep fields private and expose methods; use pub fields for plain data with no rules to protect.
  • Mark every method that changes the struct mut func, and declare the variable you call it on with mut.
  • Put constructors in a static func, especially when the value needs checking or has private fields.
  • Assignment and parameters copy a struct. Use &: when a function must change the caller's value.