Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Value structs

A struct is normally a reference to an object on the heap. A struct marked [Value] is a value instead, like a number or a tuple: it is stored inline in whatever holds it (a local, a field, an array element), and assigning it, passing it, or returning it copies it. Creating one allocates nothing.

# A value struct is stored inline and copied like a number.
[Value]
struct Span {
    var start: int;
    var end: int;

    construct(this.start = 0, this.end = 0) {}

    length(): int {
        return end - start;
    }

    # Assigns a field of `this`, so it changes the value it is called on.
    grow(by: int) {
        end = end + by;
    }
}

# A reference struct holding a value: the span is part of the line's object.
struct Line {
    var text: String;
    var span: Span;

    construct(this.text = "") {}
}

# The parameter is a copy: growing it does not change the caller's span.
grown(span: Span, by: int): Span {
    span.grow(by);
    return span;
}

main(): int {
    var a = Span(2, 5);
    var b = a;          # a copy
    b.grow(10);
    if (a.length() != 3 || b.length() != 13) {
        return 1;
    }

    var c = grown(a, 1);
    if (a.length() != 3 || c.length() != 4) {
        return 2;
    }

    # Through a reference, the change lands in the object's own span.
    var line = Line("hello");
    var same = line;
    line.span.grow(5);
    if (same.span.length() != 5) {
        return 3;
    }

    # An element read from an array is a copy: change it, then store it back.
    var spans = Array<Span>();
    spans.append(a);
    var first = spans[0];
    first.grow(1);
    spans[0] = first;
    if (spans[0].length() != 4) {
        return 4;
    }

    # Values compare field by field.
    if (a != Span(2, 5)) {
        return 5;
    }
    return 0;
}

Use a value struct for small types that describe a value rather than an identity: a span, a point, a color. An enum can be a value too (see Value enums); Optional and Result are. Keep a reference struct for anything that is shared and changed through several names, or that is large.

Declaring one

Put [Value] on its own line above the struct. Everything else is written as for any struct: fields, constructors, methods, operators, generic parameters, and extensions.

[Value]
struct Point {
    var x: int;
    var y: int;

    construct(this.x = 0, this.y = 0) {}
}

A variable or field of a value struct type that is not given a value holds the zero value: every field zero, false, or null.

Copies

  • var b = a; makes b an independent copy. Changing b leaves a as it was.
  • A parameter is a copy of the argument. A function can change its parameter without the caller seeing it; return the changed value instead.
  • A field of a value struct is part of whatever holds it. In a reference struct, line.span.grow(5) changes the span inside line’s object, and every reference to that object sees it.
  • Reading an element of an array, or the result of any call, gives a copy. To change an element, read it into a variable, change it, and store it back (spans[0] = first;).

A field that is itself a reference (a String, an Array) is copied as a reference, so both copies share that object, as two variables holding the same String do. Its count is kept like any other reference’s: copying the value acquires it, and the copy going out of scope releases it. Use copy for a deep copy.

Methods that change this

In a method, this is the value the method was called on, not a copy, so a method that assigns a field (end = end + by;), or calls another method that does, changes that value. The compiler works out which methods change this from their bodies. Calling one on something that would only change a copy is an error, because the change would be lost:

main.pep:9:12: error: cannot call mutating method 'grow' on a temporary 'Span'
    9 |     make().grow(1);
      |            ^~~~
main.pep:9:5: note: 'make' returns a copy; store it in a variable, change it, and assign it back
    9 |     make().grow(1);
      |     ^~~~

The same holds for assigning a field of a call’s result (spans[0].end = 3;) and for changing a const variable’s value. A closure made in a method holds a copy of this, so it cannot change this either.

Comparing and hashing

== and != compare two values field by field, and hash combines the fields’ hashes, unless the struct declares its own == or hash (declare both together; see Hashing and equality). A value has no identity, so === and !== are errors on it, and it is never null: comparing one with __null is an error too.

What a value struct cannot do

Each of these is an error at the code to change:

  • ===, !==, or __null on a value.
  • __weak<T> or __refcount of a value: there is no object to count.
  • A destruct block: nothing destroys a value. Its reference fields are still released.
  • [Shared]: a value struct is thread-safe when its fields are, and needs no atomic counts.
  • A field whose type is the struct itself, directly or through other value structs: the value would be infinitely large. Hold it through a reference struct or an Array instead.
  • A parameter or result of an extern function: pass a pointer to C instead.

Value structs as trait values

A value struct can declare traits like any struct. Where it flows into a slot of a trait type, such as an argument of type Iterator<int>, it is boxed: copied into a new object that the trait value refers to. The box is a copy, so a mutating method called through the trait value changes the box and not the original, and two conversions of one value make two boxes. Copying the trait value shares the box, as for any trait value.

The runtime’s iterators (ArrayIterator, StringIterator, the map and set iterators, ChannelIterator, and the map, filter, enumerate and zip adapters) are value structs, so a for loop over an Array, a String or any map or set allocates no iterator.