Structs and Types
Defining Structs
defstruct declares a named struct with typed fields:
(defstruct Point
x float64
y float64)
(defstruct User
id string
name string
email string
active bool)
Creating Struct Values
Use a map literal in a context where the type is known:
(defn make-point [x float64 y float64] -> Point
{:x x :y y})
Or use the explicit struct literal syntax:
(Point. {:x 1.0 :y 2.0})
(User. {:id "1" :name "Alice" :email "alice@example.com" :active true})
Accessing Fields
Keyword access on a struct becomes a direct field read:
(let [p (Point. {:x 3.0 :y 4.0})]
(:x p) ; 3.0
(:y p)) ; 4.0
Updating Structs
Structs are values. "Update" means creating a new struct with changed fields:
(defn move [p Point dx float64 dy float64] -> Point
{:x (+ (:x p) dx)
:y (+ (:y p) dy)})
Named Types
deftype creates a new named type based on a primitive:
(deftype UserId string)
(deftype Score int)
(deftype Percentage float64)
Named types prevent accidentally mixing values that have different meanings:
(def user-id UserId "user-123")
(def session-id string "sess-456")
; These are different types even though both are strings
Stateful Fields with Atoms
Struct fields are normally immutable values. When a struct needs its own mutable, thread-safe state, give it an atom field typed (Atom T) (or bare Atom):
(defstruct Counter
label string
count (Atom int))
(defn new-counter [name string] -> Counter
{:label name :count (atom int 0)})
(defn bump! [c Counter] -> any
(swap! (:count c) (fn [n] (+ (int n) 1))))
The struct carries its own state, so there's no need for a module-level singleton. See Atoms for atom, swap!, reset!, and deref.
Gradual Typing
Start with a plain map and upgrade to a struct as the code grows. When a function's parameter is typed as a struct, map literals in call position are automatically compiled as struct literals:
(defstruct Product
name string
price float64
stock int)
; This map literal compiles to a Product struct literal
; because the function declares Product as the parameter type
(defn price-with-tax [p Product] -> float64
(* (:price p) 1.24))
(price-with-tax {:name "Widget" :price 9.99 :stock 42})
The benefit: misspell :pricee and Go's compiler catches it at build time.
Example: A Complete Struct
(ns main)
(defstruct Circle
radius float64)
(defstruct Rect
width float64
height float64)
(defn circle-area [c Circle] -> float64
(* math/pi (:radius c) (:radius c)))
(defn rect-area [r Rect] -> float64
(* (:width r) (:height r)))
(defn main [] -> void
(let [c (Circle. {:radius 5.0})
r (Rect. {:width 4.0 :height 6.0})]
(println "circle area:" (circle-area c))
(println "rect area: " (rect-area r))))