Skip to content

The language

Structs

A struct is a named record of fields, with methods defined inside it. Structs are values like everything else in Brine: copying one copies its fields, == compares fields, and print shows them. A method that changes its struct takes ref self.

struct Point:
x: int
y: int
struct Line:
start: Point
end: Point
label: str
fn main():
let a = Point(x=1, y=2) # keyword: any order
let b = Point(3, 4) # positional: declaration order
var line = Line(label="diagonal", start=a, end=b)
line.end.y = 40 # nested fields
print(a.x + b.y)
print(line)
print(f"from {line.start} to {line.end}")
print(a == Point(1, 2), a != b)
Output
5
Line(start=Point(x=1, y=2), end=Point(x=3, y=40), label="diagonal")
from Point(x=1, y=2) to Point(x=3, y=40)
true true
  • Each field has a name and a type. Fields have no default values.
  • Construct with all fields positionally, in declaration order (Point(3, 4)), or all by keyword, in any order (Point(y=4, x=3)). Don’t mix the two.
  • Read and assign fields with a dot. Assigning needs a var; a let struct can’t change at all.

Every field needs a value:

struct Point:
x: int
y: int
fn main():
let p = Point(x=1)
print(p)
error[E0302]: missing field `y` in `Point(...)`
--> main.br:6:13
|
6 | let p = Point(x=1)
| ^^^^^^^^^^ every field needs a value
= run `brine explain E0302` for more about this error
error: could not compile due to 1 error

Methods are functions declared inside the struct. The first parameter decides what a method may do:

  • self: the method reads its receiver (a read-only copy, like any parameter).
  • ref self: the method can change its receiver. It can only be called on something you could pass as ref: a var, or an element or field of one.
  • no self: a static method, called on the type: Particle.at_rest(1.0). Use these for constructors.
struct Particle:
pos: float
vel: float
fn energy(self) -> float: # reads its receiver
return 0.5 * self.vel * self.vel
fn step(ref self, dt: float): # changes its receiver
self.pos += self.vel * dt
fn at_rest(pos: float) -> Particle: # no self: a static method
return Particle(pos=pos, vel=0.0)
fn main():
var p = Particle(pos=0.0, vel=2.0)
p.step(0.5)
p.step(0.5)
print(p, p.energy())
let q = Particle.at_rest(1.0)
print(q)
var ps = [Particle(0.0, 1.0), Particle(5.0, -1.0)]
for i in 0..len(ps):
ps[i].step(1.0) # on an array element
print(ps)
Output
Particle(pos=2.0, vel=2.0) 2.0
Particle(pos=1.0, vel=0.0)
[Particle(pos=1.0, vel=1.0), Particle(pos=4.0, vel=-1.0)]

The call p.step(0.5) doesn’t need a ref at the call site: the method’s name already says what it does, and the compiler checks that p can be changed.

struct Counter:
n: int
fn bump(ref self):
self.n += 1
fn main():
let c = Counter(n=0)
c.bump()
error[E0401]: cannot call `ref self` method `bump` on `c` because it was declared with `let`
--> main.br:9:5
|
9 | c.bump()
| ^ immutable
8 | let c = Counter(n=0)
| - declared here
|
= help: declare it with `var c = ...` to make it mutable
= run `brine explain E0401` for more about this error
error: could not compile due to 1 error

In a plain self method, self is read-only, and assigning to one of its fields is E0403. mut self isn’t allowed: it would only change a copy. To return a changed copy, build a new value: return Particle(pos=self.pos + 1.0, vel=self.vel).

Methods may have type parameters of their own; see generics.

print, str(...) and f-strings show a struct as its name and its fields, nested values included: Point(x=1, y=2). Strings inside are quoted (label="diagonal").

== and != compare two structs of the same type field by field. Structs can’t be ordered: < on two Points is E0305. A struct whose fields are all valid map keys (integers, bool, str, enums, and arrays, tuples and structs of those) can be a map key too.

A struct is a value. var q = p copies p, and passing a struct to a function passes a copy, so changing one never changes the other. (The copy is cheap: arrays and strings inside are copy-on-write.)

struct Point:
x: int
y: int
fn moved(p: Point, dx: int) -> Point:
var q = p # a copy
q.x += dx
return q
fn main():
let a = Point(1, 2)
let b = moved(a, 10)
var c = a
c.y = 0
print(a, b, c)
Output
Point(x=1, y=2) Point(x=11, y=2) Point(x=1, y=0)

To let a function change a struct the caller owns, use a ref self method or a ref parameter (see functions).

A struct can’t contain itself directly, because it would have infinite size. Put the recursive part in an array (children: [Node]) or use a recursive enum.

struct Node:
value: int
next: Node
fn main():
print(1)
error[E0309]: struct `Node` contains itself and would have infinite size
--> main.br:1:1
|
1 | struct Node:
| ^^^^^^ recursive struct
|
= help: store the recursive part inside an array or an enum, e.g. `children: [Node]`
= run `brine explain E0309` for more about this error
error: could not compile due to 1 error
  • struct Name: followed by field: Type lines and fn methods.
  • Construct positionally (Point(1, 2)) or by keyword (Point(x=1, y=2)), not mixed; every field needs a value.
  • self methods read, ref self methods change the receiver (which must be a var), methods without self are static (Type.method()).
  • Structs print as Name(field=value, ...) and compare with == field by field.
  • Copies are independent. Generic structs: see generics.