Docs/Data & ownership

Modeling data

Choose between an object, a value, and a set of alternatives.

On this page

The shape of your data determines which language feature is useful. A class gives an object identity and behavior. A struct groups fields into a value. An enum names a fixed set of choices, while a tagged union allows those choices to carry different types.

Valk
class Task {
    title: String
    done: bool (false)

    fn complete() {
        this.done = true
    }

    fn summary() String {
        return this.done ? "Done: %{this.title}" : "Todo: %{this.title}"
    }
}

fn main() {
    let task = Task { title: "Write the guide" }
    task.complete()
    println(task.summary())
}

A class initializer names its properties. A default goes in parentheses after the property type. Instance methods access the receiver with this.

Assigning a class value to another variable refers to the same object. It does not make a copy of its properties.

Use a struct for a value#

Valk
struct Point {
    x: int
    y: int
}

fn main() {
    let origin = Point { x: 0, y: 0 }
    let cursor = origin
    cursor.x = 10
    println(origin.x) // 0
    println(cursor.x) // 10
}

Struct assignment copies the fields. If a field itself holds a class reference, the copied field still refers to that same object. A struct is not an automatic deep copy of everything reachable from it.

Use a class when shared object identity is useful, and a struct when values should be copied as part of assignment or argument passing.

Represent alternatives#

An enum such as enum State { waiting active complete } is useful when every case is just a named choice. A tagged union can hold different types of data:

Valk
union Setting : String | int | bool {}

fn display(value: Setting) String {
    return match value : String {
        String as text => text
        int as number => number.to(String)
        bool as enabled => enabled ? "on" : "off"
    }
}

fn main() {
    let setting: Setting = 8080
    println(display(setting))
}

as binds the payload in a match branch. Each branch here returns a String, and every alternative is handled. The compiler can check that a match covers the union's alternatives.

Share an interface#

An interface describes operations that different classes can provide:

Valk
interface Described {
    fn describe() String;
}

class Book is Described {
    title: String

    fn describe() String {
        return "Book: %{this.title}"
    }
}

fn print_description(item: Described) {
    println(item.describe())
}

fn main() {
    print_description(Book { title: "A small guide" })
}

A class lists its interfaces after is. Its methods must satisfy the declared signatures. The interface gives a caller access to that common behavior without requiring it to know the concrete class.

Only classes implement interfaces. For reusable code that should retain a concrete type, consider generics.