Structs are named, ordered collections of fields. They are the primary way to group related data under a single type name, and they integrate cleanly with typeclasses, ownership, and the effects system.
(defstruct Name [field1 :type1 field2 :type2 ...])
Fields are listed as alternating name/type pairs inside the vector literal. All field names must be distinct.
(defstruct Point [x :int y :int])
(defstruct Pixel [r :uint8 g :uint8 b :uint8 a :uint8])
(defstruct Vec2 [x :float32 y :float32])
A defstruct at file scope registers the type immediately so that later
definitions -- including self-referential or mutually-recursive structs -- can
refer to it.
Every struct has a copy kind that governs how the value can be used:
| Annotation | Meaning |
|---|---|
:copy |
Bitwise-copyable; can be freely duplicated (all fields must be copy types) |
:move |
Move-only (default when no annotation is given) |
:linear |
Exactly-once consumed; must be used exactly once |
(defstruct Point :copy [x :int y :int]) ; freely copyable
(defstruct Packet :move [ptr :ptr<void>]) ; ownership transferred on use
(defstruct Socket :linear [fd :int]) ; must be consumed exactly once
Omitting the annotation is the same as :move:
(defstruct Pair [first :int second :int]) ; move-only by default
The compiler enforces that a :copy struct's fields are all themselves
copy-compatible types. Using a non-copy field (such as :ref<int>) in a
:copy struct is a compile error.
| Syntax | Meaning |
|---|---|
:int |
64-bit signed integer |
:bool |
Boolean |
:float |
64-bit float |
:float32 |
32-bit float |
:uint8, :int16, :int32, … |
Sized numeric types |
:cstr |
C string pointer (const char *) |
:ptr<void> |
Raw pointer |
:rc<T> |
Reference-counted pointer to T |
:ref<T> |
Owned reference to T (non-RC) |
:lref<T> |
Linear reference to T (exactly-once) |
:weak<T> |
Weak reference (non-owning, for rc<T> cycles) |
:fn |
Function pointer |
:fn #{Effect} |
Function pointer with an explicit effect-row annotation |
Drop glue (automatic cleanup of rc<T>, ref<T>, and weak<T> fields) is
generated by the compiler whenever any such field is present.
Use make-struct to construct a struct value. Fields are supplied positionally
in the order they appear in defstruct.
(defstruct Point :copy [x :int y :int])
(let [p (make-struct Point 3 4)]
...)
(defstruct Pixel [r :uint8 g :uint8 b :uint8 a :uint8])
(let [px (make-struct Pixel 255u8 128u8 64u8 255u8)]
...)
The argument count must exactly match the field count; a mismatch is a compile-time error.
Use .fieldname to read a field from a struct value:
(defstruct Point :copy [x :int y :int])
(let [p (make-struct Point 3 4)]
(println (.x p)) ; 3
(println (.y p))) ; 4
The .fieldname form is also valid for arithmetic:
(defn distance-sq [p] :int
(+ (* (.x p) (.x p))
(* (.y p) (.y p))))
Prefix .fieldname with & to take an immutable borrow of a field without
moving the struct:
(defstruct Point :copy [x :int y :int])
(let [p (make-struct Point 3 4)]
(let [rx &(.x p)]
(println @rx))) ; prints 3
lref<T>)A :move struct may contain :lref<T> fields. Extracting such a field
transfers linear ownership out of the struct and marks the struct binding as
moved. A second extraction from the same binding is a use-after-move error
(TUR-E0005).
(defstruct PtrBox :move [ptr :lref<int>])
;; OK: single extraction
(let [b (make-struct PtrBox (lref/new 42))]
(println (deref (.ptr b))))
;; ERROR TUR-E0005: second extraction of a moved struct
(defstruct Box :move [val :lref<int>])
(let [b (make-struct Box (lref/new 42))]
(let [x (.val b)]
(let [y (.val b)] ; use-after-move
(println (deref x)))))
Wrap a struct in rc/of to heap-allocate it with reference counting. The RC
wrapper manages drop glue automatically when the count reaches zero.
(defstruct Wrapper :move [val :rc<int>])
(let [inner (rc/of 10)]
(let [w (rc/of (make-struct Wrapper inner))]
(println (rc/strong-count w))))
If the struct itself contains :rc<T> fields, the compiler generates nested
drop glue to decrement those inner counts:
(defstruct Node :move [val :rc<int>])
(let [inner (rc/of 42)
outer (rc/of (make-struct Node inner))]
(println (rc/strong-count outer)))
Implement typeclasses for your struct types with definstance. Inside inline-C
methods, struct parameters arrive as C struct values, so field access uses dot
notation.
Clone(defstruct Pair [first :int second :int])
(definstance Clone [Pair]
(clone [x] :int
```c
struct { int64_t first; int64_t second; } *dst = malloc(sizeof(Pair));
dst->first = x.first;
dst->second = x.second;
return (int64_t)(intptr_t)dst;
```))
Eq(definstance Eq [Pair]
(eq? [x y] (pair-eq? x y (fn [a b] (= a b)))))
Show(defstruct Point :copy [x :int y :int])
(definstance Show [Point]
(show [__p] :cstr
```c
int nx = snprintf(NULL, 0, "%lld", (long long)__p.x);
int ny = snprintf(NULL, 0, "%lld", (long long)__p.y);
size_t len = 13 + (size_t)nx + 6 + (size_t)ny + 3 + 1;
char *buf = (char *)malloc(len);
snprintf(buf, len, "Point { x = %lld, y = %lld }",
(long long)__p.x, (long long)__p.y);
return (const char *)(intptr_t)buf;
```))
(let [p (make-struct Point 3 4)]
(println (.show p))) ; Point { x = 3, y = 4 }
Show, Debug, DisplayWriting the inline C above for every struct is tedious. The derive-show,
derive-debug, and derive-display macros in stdlib/macros.tur generate
the instance from a struct name and a list of field descriptors.
Each field descriptor is either a bare symbol -- x becomes label "x" with
accessor (.x s) -- or a [label .accessor] pair to use a different label
or non-default accessor.
(defstruct Point :copy [x :int y :int])
(derive-show Point x y)
(derive-debug Point x y)
(derive-display Point x y)
(let [p (make-struct Point 3 4)]
(println (.show p)) ; Point { x = 3, y = 4 }
(println (.debug p)) ; (Point (x 3) (y 4))
(println (.display p))) ; Point { x = 3, y = 4 }
derive-show produces "TypeName { k = v, ... }". derive-debug produces
the s-expression-flavoured "(TypeName (k v) ...)". derive-display matches
derive-show but dispatches through the Display typeclass.
Each field must itself have an instance of the relevant typeclass -- the macro
expansion calls .show (or .debug/.display) on each field value and joins
the results with str-concat. Show, Debug, and Display instances exist
in stdlib for primitives, Pair, Option, Result, List, and Vec, so
nested structs and collections print recursively out of the box.
To alias a field name or access through a non-standard reader, use the
[label .accessor] pair form:
(defstruct MyStruct [name :cstr internal-label :cstr count :int])
(derive-show MyStruct name [display-name .internal-label] count)
;; => "MyStruct { name = ..., display-name = ..., count = ... }"
The native REPL and web REPL automatically call show on the result of each
top-level expression when an applicable Show instance exists, so:
> (make-struct Point 3 4)
Point { x = 3, y = 4 }
If no Show instance is registered for the result type, the REPL falls back
to printing the raw value. Note: heap-allocated stdlib types returned through
constructors that elaborate to :int (for example pair-new) are seen by
typeclass dispatch as int and will print as a raw pointer; use
(make-struct Pair 1 2) (or a typed alias) when you want Show-dispatch to
fire on the constructed value.
Bifunctor(definstance Bifunctor [Pair]
(bimap [container fn-left fn-right]
(__bifunctor_pair_bimap container fn-left fn-right)))
A :fn field stores a function pointer. Annotate the field with #{Effect}
to declare the effect row the stored function may perform. Calling the field
propagates that effect row to the enclosing function.
(defeffect Emit [s :cstr] :nil)
(defstruct Emitter :copy [run :fn #{Emit}])
(defn main [] :int
(let [em (make-struct Emitter (fn [s] (perform (Emit s))))]
(handle
(do (.run em "hello") 0)
(Emit [s] k) (do (println s) (resume k nil)))))
A struct defined in one module can be imported and used in another. Construct
it with make-struct and access its fields with .fieldname exactly as if it
were locally defined.
;; geom module defines Point
(defmodule app
(import geom :refer [Point])
(defn main [] :int
(let [p (make-struct Point 3 4)]
(println (+ (.x p) (.y p)))
0)))
Follow the ;;; convention immediately above the defstruct form:
;;; Point -- a 2D integer coordinate.
;;;
;;; Parameters:
;;; x -- horizontal position
;;; y -- vertical position
;;;
;;; Example:
;;; (make-struct Point 3 4) ; => Point with x=3, y=4
;;;
;;; Since: Phase B1
(defstruct Point :copy [x :int y :int])
See the docstring standard in CLAUDE.md for the full required-fields table.
| Error | Cause |
|---|---|
TUR-E0005 |
Use-after-move: reading a field from a struct that was already moved (or extracting a linear field twice) |
| Field count mismatch | make-struct given fewer or more arguments than the struct has fields |
:copy constraint violation |
A :copy struct contains a field type that is not copy-compatible (e.g. :ref<int>) |
^linear, ^affine, ^relevant