Home Documentation Templates Examples Showcase GitHub
Theme

Core language · State & behaviour

Structs.

Structs combine fixed, stably typed state with methods. Instances have reference semantics, members are public by default, and private can hide fields or methods.

Define and construct a struct

@struct(point) {
    x := 0
    y := 0

    fn(point(x_, y_)) {
        x = x_
        y = y_
    }

    fn(length_squared()) {
        return x * x + y * y
    }
}

$[p := point(3, 4)]
$[p.x]
$[p.length_squared()]

Every field is declared with an initial value, which establishes its stable inferred type. A struct may have at most one constructor: a method whose name matches the struct name.

State and behaviour

@struct(stats) {
    private count := 0
    private total := 0.0

    fn(add(value)) {
        count = count + 1
        total = total + value
    }

    fn(average()) {
        if(count == 0) { return 0.0 }
        return total / count
    }
}

Methods use the same statement grammar as functions, including if, for, while, break, continue and optional returns.

Real-world examples

Structs are most useful when a build-time task has a small piece of state and the operations on that state belong together. They are not limited to modelling abstract data structures.

Statistics accumulator

@struct(stats) {
    private count := 0
    private total := 0.0

    fn(add(value)) {
        count = count + 1
        total = total + value
    }

    fn(count()) { return count }
    fn(total()) { return total }

    fn(average()) {
        if(count == 0) { return 0.0 }
        return total / count
    }
}

$[scores := stats()]
$[scores.add(8.5)]
$[scores.add(9.0)]
$[scores.add(7.5)]

<p>Average: $[scores.average()]</p>

This keeps the count, total and averaging rule together instead of coordinating several unrelated outer bindings.

Site configuration abstraction

@struct(site_config) {
    title := ""
    base_url := ""
    private production := false

    fn(site_config(title_, base_url_, production_)) {
        title = title_
        base_url = base_url_
        production = production_
    }

    fn(is_production()) {
        return production
    }
}

$[site := site_config("Example", "https://example.com", true)]

<h1>$[site.title]</h1>
@if(site.is_production()) {
    <p>Production build</p>
}

A configuration struct can expose the values templates need while keeping implementation state private and attaching useful queries to the same instance.

Shared traversal state

@struct(counter) {
    private value := 0

    fn(next()) {
        value = value + 1
        return value
    }
}

$[sequence := counter()]

@for(item : items) {
    <article id="item-$[sequence.next()]">
        <h2>$[item.title]</h2>
    </article>
}

Because struct instances have reference semantics, the same counter can be passed through functions or fragments and still represent one shared traversal state without a global variable.

Recursive fragment state

@struct(menu_state) {
    private depth := 0

    fn(enter()) { depth = depth + 1 }
    fn(leave()) { depth = depth - 1 }
    fn(depth()) { return depth }
}

@fragment(menu(items, state)) {
    $[state.enter()]
    <ul data-depth="$[state.depth()]">
        @for(item : items) {
            <li>$[item.title]</li>
        }
    </ul>
    $[state.leave()]
}

This is useful when recursive rendering needs shared structured state: pass one instance rather than adding more scalar parameters at every recursion level.

Why no navigation-builder push() example?

Nift arrays do not currently have in-place push/pop/remove methods. Struct methods can mutate their fields, but examples should not imply a container-mutation API that the language does not provide. Use collection operations for array transformations.

Private fields and methods

@struct(counter) {
    private count := 0
    fn(counter(start)) { count = start }
    private fn(step()) { count = count + 1 }

    fn(add(n)) {
        i := 0
        while(i < n) { step(); i = i + 1 }
    }

    fn(value()) { return count }
}

Fields and methods are public unless marked private. Private members are available to methods of the struct but inaccessible through an instance from outside it.

Implicit members and this

@struct(counter) {
    value := 0
    fn(set(value)) {
        this.value = value
    }
}

Fields and methods are implicitly visible inside methods. Parameters and local bindings take precedence, so this explicitly names the receiver when a local name shadows a member.

Reference semantics

$[a := counter(2)]
$[b := a]
$[b.add(3)]

<p>$[a.value()]</p>  <!-- 5: a and b refer to the same instance -->

Assigning or passing a struct instance copies its reference, not the entire instance.

Shallow and recursive copies

$[alias := original]
$[shallow := copy(original)]
$[independent := deepcopy(original)]

copy() creates a distinct struct instance and shallow-copies its fields according to their normal value/reference semantics. deepcopy() recursively duplicates mutable aggregate state. Private fields participate because copying is runtime state duplication, not public serialization.

Fixed shape and stable field types

Struct fields cannot be added or removed dynamically. Assigning a value of a different type to a field is an error, just as it is for ordinary stable inferred bindings. Field typing is lexical like ordinary bindings: count := 0 infers int while total := 0.0 infers double, and arithmetic keeps a field double whenever any operand is double (see Declarations & assignments).

Scope

Struct definitions and method execution follow normal lexical scoping. Methods can see the surrounding bindings available through their definition/call context while instance members remain per-instance state.