A struct holds one or more named values (fields), each with its own type. The struct's fields are located together in memory. A field's value can be individually accessed using the field's name.

Let's illustrate by declaring a new struct type:

struct Complex:
  real f32
  imag f32

This defines a new struct type called Complex that holds two fields of data. The fields named real and imag are both 32-bit floating point numbers.

Variables can be declared and initialized using this new type, each with its own values in these two fields:

imm comp1 = Complex[real: 3.2, imag: 0.0]
mut comp2 = Complex[real: 0.0, imag: 1.0]

This page focuses on using structs to hold field-oriented data. Subsequent pages will enrich what can be done with a struct by adding methods, inheritance, or traits.

Declaration

Before we can make use of a specific data structure, we need to declare it, giving it a name and specifying its fields:

struct Point:
    x f32 = 0.
    y f32 = 0.

The struct's declared name is effectively new, custom-defined type. This type name can be used to declare any number of variables containing values of this type (e.g., imm pt1 Point).

Field declarations look like variable declarations, specifying the field's:

Initialization

The initial value for any struct value is always built (explicitly or implicitly) using a struct constructor: the struct name followed by comma-separated field values in square brackets. Values may be marked with field names or just listed in declared order:

imm pt = Point [x: 3., y: 4.]    // Using field names
imm pt2 = Point [3., 4.]         // When field names are omitted, order matters.
imm pt3 = Point[]                // Uses x,y's declared default values of 0.

Several constraints apply to struct constructors:

It is also possible to initialize a new struct value by using one of the struct's defined initializer methods instead. Under the covers, initializer methods also use the struct constructor to initialize the struct's value.

Copying

Like numbers, struct values may be passed around between variables and functions:

fn main():
	mut pt1 = Point[]
	pt1 = change(pt1)

fn change(mut pt Point) Point:
	pt = Point[1., 2.]
	pt

Copies of the data structures are made as they are passed around (e.g. into and returned by change). Any changes made to a copy won't change the original struct value. This may not always be the behavior that you want, though. If a struct is too big to pass around efficiently, or you want changes to apply to the source structure, make use of references to a struct instead.

Field Access

Use the . operator to access a struct field's value, specifying the struct value on the left and the field name on the right:

imm dist = (pt.x*pt.x + pt.y*pt.y).sqrt

A field's value may also be altered in this way:

pt.y = pt.x

The compiler applies field privacy and permission constraints to prevent invalid field accesses. A private field can only be accessed by the type's methods. Changing of a field's value is only allowed if the struct/field permissions allow mutability.

It is also possible to use the & operator to borrow a reference to a specific field within a struct value.

Comparing

It is not possible to check whether two structs have the same value if the struct defines only fields. However, two struct values may be compared if the struct implements the comparison methods. Also, it is possible to perform partial matching of a struct against some pattern.

Empty structs

It is perfectly legal to define a struct with no fields. Nearly always, such types may be used like any struct type for variables, fields, parameters, etc.:

The most significant restriction is that it is not possible to allocate a zero-sized value on the heap, which means you cannot create an owning reference to a value of an empty struct.

The language's pre-defined void type is defined internally as an empty struct.

Opaque Types

Sometimes it is valuable to work with types where we have no idea about the size or layout of any values of that type. This is useful when interfacing with some external system which knows the layout and size of the type's data, but is reluctant to share (or we don't really care).

We call such types opaque, because we cannot directly view their contents. Declaring an opaque type is easy:

struct @opaque SocketHandle
   // don't specify any fields as part of the type
   // Methods may be defined.

As one might expect, it is illegal for a variable or field to specify an opaque type. An opaque value cannot be created or passed around. We cannot access its fields. We cannot even determine its size.

Where opaque types are used is when pointed-at by references or pointers. Such references can be obtained, passed around, and stored, like any reference. We can call methods on a pointed-at value of an opaque type (typically implemented by the external system that knows the data layout and can work with such values). However, we may not use pointer de-referencing to read or mutate the pointed-at value.

_