Structs

Defining and using structs in Chasm, value semantics, default fields, struct update with with, and arrays of structs.

A struct groups related fields into a named type. Structs are value types, they behave like integers and floats, not like objects or pointers.

Defining a Struct

defstruct Vec2 do
  x :: float
  y :: float
end

Fields are listed one per line: name :: type. Order is significant, it determines the order in the struct literal.

Creating Instances

Write the struct name followed by { field: value, ... }:

pos    = Vec2{ x: 3.0, y: 4.0 }
origin = Vec2{ x: 0.0, y: 0.0 }

All fields must be provided unless they have default values (see below).

Value Semantics, What "Value Type" Means

This is the most important thing to understand about structs. When you assign a struct to another variable, or pass it to a function, a full copy is made. There are no shared references.

a = Vec2{ x: 1.0, y: 2.0 }
b = a             # b is an independent copy of a

b = b with { x: 99.0 }   # modifying b does NOT affect a

print(a.x)        # 1.0, a is unchanged
print(b.x)        # 99.0

This is different from most scripting languages where objects are references. In Chasm, b = a means "give b its own private copy of everything in a". This makes reasoning about game state much simpler, there are no aliasing surprises.

Default Field Values

Fields can declare a default value. When a field is omitted in a struct literal, the default is used:

defstruct Bullet do
  x      :: float = 0.0
  y      :: float = 0.0
  speed  :: float = 5.0
  active :: int   = 0
end

b1 = Bullet{}              # all defaults: x=0, y=0, speed=5, active=0
b2 = Bullet{ x: 10.0 }    # x=10, rest use defaults
b3 = Bullet{ x: 5.0, speed: 8.0 }

Bullet{} is valid because every field has a default. Without defaults, all fields are required.

Struct Update with with

The with keyword creates a modified copy of a struct. Only the listed fields change, all others are copied from the original:

pos  = Vec2{ x: 1.0, y: 2.0 }
pos2 = pos with { x: 10.0 }   # Vec2{ x: 10.0, y: 2.0 }

This is the idiomatic way to update a struct. It is equivalent to writing the full literal by hand:

# These two are identical:
pos2 = pos with { x: 10.0 }
pos2 = Vec2{ x: 10.0, y: pos.y }

with is especially useful when updating a struct stored in an array. Read it, modify it, write it back:

b = @bullets.get(i)
@bullets.set(i, b with { x: b.x + b.speed * dt })

Reading Fields

Access a field with .:

pos = Vec2{ x: 3.0, y: 4.0 }
len = sqrt(pos.x * pos.x + pos.y * pos.y)   # 5.0

Passing Structs to Functions

Structs are passed by value, the function receives its own copy:

defp length(v :: Vec2) :: float do
  sqrt(v.x * v.x + v.y * v.y)
end

defp normalized(v :: Vec2) :: Vec2 do
  len = length(v)
  Vec2{ x: v.x / len, y: v.y / len }
end

def main() do
  pos = Vec2{ x: 3.0, y: 4.0 }
  n   = normalized(pos)
  print(n.x)   # 0.6
  print(n.y)   # 0.8
end

The function cannot mutate the caller's struct, it works on its own copy.

Arrays of Structs

Use array_new(Type, cap) for local struct collections inside functions:

defp find_nearby(entities :: []Entity, px :: float, py :: float, r :: float) :: []Entity do
  result = array_new(Entity, entities.len)
  for e in entities do
    if vec2_dist(e.x, e.y, px, py) < r do
      result.push(e)
    end
  end
  return result
end

Use array_fixed(cap, StructLiteral{}) for module-level arrays:

defstruct Enemy do
  x      :: float = 0.0
  y      :: float = 0.0
  hp     :: int   = 10
  active :: int   = 0
end

@enemies :: script = array_fixed(32, Enemy{})

All 32 slots are pre-filled with Enemy{}, x=0, y=0, hp=10, active=0. On hot-reload the script arena resets and all slots reinitialize from the default automatically.

To update a slot, read-modify-write:

def on_tick(dt :: float) do
  i = 0
  while i < @enemies.len do
    e = @enemies.get(i)
    if e.active == 1 do
      @enemies.set(i, e with { x: e.x + 2.0 * dt, hp: e.hp - 1 })
    end
    i = i + 1
  end
end

Nested Structs

Struct fields can be other structs:

defstruct Transform do
  pos :: Vec2
  rot :: float
end

t = Transform{ pos: Vec2{ x: 5.0, y: 3.0 }, rot: 0.0 }
print(t.pos.x)   # 5.0

# Update nested field with with
t2 = t with { pos: t.pos with { x: 10.0 } }
print(t2.pos.x)   # 10.0
print(t2.rot)     # 0.0 , unchanged