Skip to content

Struct

There are no classes in the language, so structs are the next best option. Structs can be defined using the struct keyword and a label, which will then be used as the "type".

struct Fraction {
  int numerator,
  int denominator,
}

Fraction myFrac = Fraction{.numerator = 10, .denominator = 31}

Members of a struct can be accessed with <struct>.<member>.

If your struct is mutable, you can update any of its members:

mut Fraction myFrac = Fraction{.numerator = 1, .denominator = 10}
myFrac.numerator *= 2

assert(myFrac == Fraction{.numerator = 2, .denominator = 10})

Structs can be exported from their module using the pub keyword, and all of their fields will be public as well.

Methods

You can define methods on structs in the same way that you define functions. There are 3 types of methods: static, non-mutating, and mutating.

There are some special keywords available for use in methods. The self keyword represents an instance of the struct. It can be used to access fields and non-static methods on the struct. The Self type represents the struct type, and is simply a placeholder for the struct type name.

Unlike fields, methods of an exported struct are not exported by default, and must be specifically exported with the pub keyword. Combining everything together, there are 6 possible types of struct methods:

  • fn(): static method
  • pub fn(): exported static method
  • fn(self): non-mutating method
  • pub fn(self): exported non-mutating method
  • mut fn(self): mutating method
  • pub mut fn(self): exported mutating method

Also note that methods can be overloaded, meaning that multiple methods can exist with the same name, provided they have different function signatures. The mut and pub modifiers do not count in the function signature, so these two methods:

struct Data {
  fn method(self) -> int {
    return 1
  }

  mut fn method(self) -> int {
    return 1
  }
}

are considered to have the same function signature.

Static methods

Static methods are functions inside a struct that do not have self as an argument. Functionally, these are equivalent to normal functions, and only differ in their ability to use the Self type in their definition.

struct Fraction {
  int numerator,
  int denominator,

  fn new(int n, int d) -> Self {
    return Self{.numerator = n, .denominator = d}
  }
}

You can call static methods using the Type.method() syntax. For the example above:

Fraction f = Fraction.new(1, 2)
assert(f == Fraction{.numerator = 1, .denominator = 2})

Static methods cannot be called from struct instances. So in the example above, f.new() will error at compile time.

Non-mutating methods

These are methods that use the self keyword, but do not mutate the struct instance. A good use case for these is to chain methods together, or if you want to return a result using the struct fields. For instance:

import "std/math" as math

struct Vector {
  int x,
  int y,

  fn mod(self) -> rat {
    return math.sqrt(x**2 + y**2)
  }

  fn add1x(self) -> Self {
    return Self{.x = self.x + 1, .y = self.y}
  }
  fn add1y(self) -> Self {
    return Self{.x = self.x, .y = self.y + 1}
  }
}

Vector vec = Vector{.x = 3, .y = 4}
assert(vec.mod() == 5.0)

assert(vec.add1x().add1y() == Vector{.x = 4, .y = 5})

Mutating methods

These methods mutate the struct instance and can only be called on mut instances. The function definition must also have the mut keyword to indicate that the method is mutating. Only self is mutable in the function, all of the other arguments are still immutable.

struct Vector {
  int x,
  int y,

  mut fn add(self, Self other) {
    self.x += other.x
    self.y += other.y
  }
}

mut Vector a = Vector{.x = 1, .y = 2}
Vector b = Vector{.x = 3, .y = 4}

a.add(b)
assert(a == Vector{.x = 4, .y = 6})