Cone Language Reference

structs

On this page

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 {
  pub real f32;
  pub 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 = new Complex(real: 3.2, imag: 0.0);
mut comp2 = new 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 {
    pub x f32 = 0.;
    pub 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:

  • Permission. The actual permission that governs field access is derived from both the field's and struct's permissions using a mechanism called viewpoint adaptation. If unspecified, mut is assumed, which effectively means the struct value's permission governs access.
  • Name. Each field must be given a unique name. However, names used in one struct will not be confused with same-named fields in other types, nor with variable or function names.

    A field is private unless it is declared pub. A private field is private to the module that declares the struct: any of that module's code may access it, through any value of the struct, and code outside the module may not. Fields should be left private if we don't want logic outside the module to depend on this implementation detail or be able to change its value directly. This is particularly important when the field's value should be subject to additional invariant constraints that are properly enforced by the struct's methods. A struct whose fields must be kept even from the rest of its module is declared in a module of its own.

    A pub field of a pub struct may not have a type private to the module, since code outside the module would reach into a type it cannot name.

    The anonymous name _ may be used for any data segment of the struct that we don't want or need access to.

  • Type. This can be any type: numeric, bool, an array, some other struct, etc. The name Self may be used to refer to the struct type currently being defined. There is one important restriction: A field's type may not be the same as the struct type we are declaring (or any recursive variation on this). A field's type, however, may be a reference to the struct type, or to a type that holds it, whichever of them is declared first: a reference is a pointer whatever it points at.
  • Fold clause. A public field may end with use and a list of members of its type, which become names of this struct reached through the field: delegated inheritance.
  • Default value. The default value must be a constant: a literal, null for a pointer field among them, a named constant, a constant expression computed from them (Red | Green), or a reinterpretation of one to a number or pointer type with as, such as 4096usize as *u8, or an array, struct or tuple of such constants. Specifying default values makes initialization of a newly created struct more convenient.

Initialization#

A new struct value is constructed with new: the struct name followed by the arguments of one of its initializers in parentheses. Every struct has an implicit initializer that takes its fields' values. Values may be marked with field names or just listed in declared order:

imm pt = new Point(x: 3., y: 4.);   // Using field names
imm pt2 = new Point(3., 4.);        // When field names are omitted, order matters.
imm pt3 = new Point();              // Uses x,y's declared default values of 0.
imm pt4 = new Point;                // The same: empty parentheses may be left off

Several constraints apply to the implicit initializer:

  • It must be given a value for every field lacking a default value.
  • Only code in the module that declares the type may use it to give a value to a private field. A private field with a default value may be left to its default from anywhere.

A struct may also declare initializer methods of its own, which may compute the value they build. The arguments of a construction select the initializer that takes them. Where a declared initializer and the implicit one both take values listed in order, the declared one is used; naming the fields reaches the implicit one.

A construction fills its value where it goes: declaring a variable with one hands the initializer the variable's own memory, so no copy is made.

Copying#

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

fn main() {
	mut pt1 = new Point();
	pt1 = change(pt1);
}

fn change(mut pt Point) Point {
	pt = new 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 code in the module that declares the type. 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.

Static variables#

A field is held by every value of the struct, one copy each. A variable declared static inside the struct is one copy shared by all of them. It is not a field: no value carries it and no initializer sets it. From outside the struct it is reached through the type's name, in the same way as the struct's functions; inside the struct's own methods and functions it is reached by its bare name.

struct Tally {
  pub n i32;
  pub static mut made = 0;

  pub fn make(n i32) Tally {
    made = made + 1;       // shared by every Tally
    new Tally(n);
  }
}

imm a = Tally.make(1);
imm b = Tally.make(2);
imm count = Tally.made;   // 2

Like a global variable, a static must be initialized with a constant value (a literal, a named constant or a constant expression), and it obeys pub as any other member of the struct does. A generic struct's static belongs to each instantiation separately: Crate[i32] and Crate[f32] are two types, and each has its own copy.

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.:

  • Although its values take up no space, empty struct values can nonetheless be passed around.
  • Methods may be defined for an empty struct and used by its values.

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.