Struct
Structs are user-defined record types: a named collection of typed fields.
struct Point {
x: Int,
y: Int,
}
let p = Point(1, 2);
assert_eq(p.x, 1);
p.y = 20;
assert_eq(p, Point(1, 20));
Declaring a struct
A declaration names the struct and lists its fields. Every field requires a type annotation, and a trailing comma after the last field is allowed. A struct may have zero fields.
struct Person {
name: String,
age: Int,
locations: List<String>,
}
struct Marker { }
Declare a struct on its own line, like a let declaration — at the top level of
a program, inside a block, or in the REPL. A struct cannot be declared in the
middle of another expression, so let s = struct P { x: Int } is an error.
Struct names are lexically scoped, like variables. Declaring two structs with the same name in the same scope is an error, but the name can be reused in scopes that never coexist, and a declaration in an inner scope shadows a same-named struct from an outer scope.
struct Point { x: Int }
struct Point { y: Int } // ERROR: Illegal redefinition of struct 'Point'
A struct declared inside a function or block goes out of scope with it: the type name, the constructor, and the field accessors are all unavailable outside.
A struct cannot take the name of a built-in type:
struct Int { x: Float } // ERROR: Struct 'Int' is not allowed to shadow the built-in type 'Int'
A struct can only be used after its declaration, and the name becomes usable as a type annotation in the scopes where the struct is visible.
Constructing instances
Declaring a struct binds a constructor function with the same name. It takes the field values positionally, in declaration order.
struct Point { x: Int, y: Int }
let p = Point(1, 2);
Constructor calls are checked before the program runs: passing the wrong number of arguments or incompatible types is a compile-time error.
Point(1); // ERROR: no 'Point' matches the arguments 'Int'
Point("x", 2); // ERROR: no 'Point' matches the arguments 'String, Int'
Field access
p.x reads the field x from p. This is not special syntax for structs:
declaring a struct binds an ordinary getter function per field, and p.x is
exactly the call x(p). Method call syntax
works too, so p.x() is the same call again.
struct Point { x: Int, y: Int }
let p = Point(1, 2);
assert_eq(p.x, 1);
assert_eq(x(p), 1);
assert_eq(p.x(), 1);
Because getters are ordinary function values you can pass them to higher-order functions:
let points = [Point(1, 10), Point(2, 20)];
assert_eq(points.map(x), [1, 2]);
assert_eq(points.map(fn (p) => p.x), points.map(x));
// The constructor is a function value too.
struct Wrap { v: Int }
assert_eq([1, 2, 3].map(Wrap), [Wrap(1), Wrap(2), Wrap(3)]);
Accessors are resolved by overloading, so two structs can share a field name without interfering:
struct Foo { size: Int }
struct Bar { size: Int }
assert_eq(Foo(1).size, 1);
assert_eq(Bar(10).size, 10);
Because s.f() is method-call syntax, calling a function stored in a field
needs parentheses around the member access: (s.f)() first evaluates s.f
(the getter) and then calls its result.
struct Callback { f: Any }
let cb = Callback(fn (a) => a * 2);
cb.f(21); // ERROR: this is method-call syntax for `f(cb, 21)`
(cb.f)(21) // 42: reads the field, then calls the stored function
Field assignment
p.x = value writes to a field. The value must fit the field’s declared type:
struct Point { x: Int }
let p = Point(1);
p.x = 10; // fine
p.x = "ten"; // ERROR: mismatched types: found String but expected Int
Augmented assignment works on fields and evaluates the receiver expression exactly once:
struct Counter { hits: Int }
let c = Counter(1);
c.hits += 4;
assert_eq(c.hits, 5);
A field is a typed location, so an augmented assignment whose result would not fit the field type is rejected:
c.hits += 0.5; // ERROR: mismatched types: found Float but expected Int
Reference semantics
Struct instances are passed by reference, like lists and maps (see Memory Management). Assigning an instance to another variable aliases it rather than copying it:
struct Point { x: Int, y: Int }
let a = Point(1, 2);
let b = a;
b.x = 99;
assert_eq(a.x, 99);
Use clone for an independent instance (nested containers are still shared,
like cloning a list of lists) or deepcopy to duplicate nested mutable state
as well:
let c = clone(a);
c.x = 1;
assert_eq(a.x, 99);
assert_eq(c.x, 1);
Equality and hashing
Typing is nominal: instances of the same struct compare field by field, and instances of different structs are never equal, even when the fields match.
struct Foo { v: Int }
struct Bar { v: Int }
assert_eq(Foo(1) == Foo(1), true);
assert_eq(Foo(1) == Foo(2), false);
assert_eq(Foo(1) == Bar(1), false);
Instances are hashable, so they work as map keys and set members:
struct Point { x: Int, y: Int }
let visited = %{Point(0, 0): true};
assert_eq(visited[Point(0, 0)], true);
Structs and JSON
json_encode rejects structs, because the struct type would be lost: a JSON
object decodes back to a map, not a struct. Use json_encode_lossy to encode an
instance as a JSON object with the field names as keys.
struct Point { x: Int, y: Int }
json_encode_lossy(Point(1, 2)); // "{\"x\":1,\"y\":2}"
json_encode(Point(1, 2)); // ERROR: cannot convert a struct to JSON
Current limitations
- Constructors are positional only; there is no named-field or default-value syntax.
- Structs do not take generic parameters:
Point<Int>is an error. - A struct cannot reference itself in its own field types.
struct Node { next: Option<Node> }fails withunknown type, because the name is only registered after its field annotations are resolved.