Structs
Structs are key-value maps with keyword keys. {...} is immutable; @{...} is mutable.
Literals
{:name "Alice" :age 30} # immutable struct
@{:name "Alice" :age 30} # mutable @struct
Access
(def user {:name "Alice" :age 30 :role :admin})
(get user :name) # => "Alice"
(get user :missing :nope) # => :nope (default value)
(has? user :age) # => true
(length user) # => 3
# callable struct syntax
(user :name) # => "Alice"
# accessor syntax: obj:field is sugar for (get obj :field)
user:name # => "Alice"
user:role # => :admin
The receiver must be a symbol
obj:field is one token. The reader absorbs the : while reading a symbol, so the sugar never applies to an expression. After a closing parenthesis the :field lexes as a plain keyword instead, and the form becomes callable-struct syntax — with the remaining arguments read as the default value.
For a plain lookup the two agree, so nothing looks wrong:
(def user {:name "Alice" :age 30})
(def outer {:inner user})
user:name # => "Alice"
((get outer :inner):name) # => "Alice"
They part company when the field holds a function. With a symbol receiver the sugar calls it. With an expression receiver there is no call: the field comes back, and the arguments are silently discarded.
(def obj {:double (fn [x] (* x 2))})
(def holder {:obj obj})
(obj:double 21) # => 42, called
((get holder :obj):double 21) # => <closure>, NOT called
Bind the receiver to a name before using the sugar:
(let [o (get holder :obj)]
(o:double 21)) # => 42
Immutable updates
Operations on immutable structs return new structs. The original is unchanged. Both put and del return the resulting struct (new for immutable, same reference for mutable).
(def user {:name "Alice" :age 30})
(put user :email "a@b.com") # => {:name "Alice" :age 30 :email "a@b.com"}
(del user :age) # => {:name "Alice"}
(update user :age inc) # => {:name "Alice" :age 31}
(merge user {:age 31 :role :admin})
# => {:name "Alice" :age 31 :role :admin}
Introspection
(keys {:a 1 :b 2}) # => (:a :b) (deterministic key order)
(values {:a 1 :b 2}) # => (1 2) (matching key order)
(pairs {:a 1 :b 2}) # => ([a 1] [b 2]) (list of [key value] arrays)
(from-pairs [[:a 1] [:b 2]]) # => {:a 1 :b 2}
pairs returns a list of [key value] arrays in the struct's key order. Key order is deterministic — the same in every run, thread, and process — but carries no alphabetical meaning: symbol and keyword keys sort by their name hash (docs/impl/symbol.md § "What the property buys"). Combined with from-pairs, structs can be round-tripped through list operations:
(def s {:x 1 :y 2 :z 3})
(from-pairs (filter (fn [p] (> (get p 1) 1)) (pairs s)))
# => {:y 2 :z 3}
Nested access and update
(def config {:db {:host "localhost" :port 5432}})
(get-in config [:db :host]) # => "localhost"
(put-in config [:db :port] 3306) # => {:db {:host "localhost" :port 3306}}
(update-in config [:db :port] inc) # => {:db {:host "localhost" :port 5433}}
Mutable @structs
put and del on @struct mutate in place and return the same struct.
(def tbl @{:count 0})
(put tbl :count 1) # mutates tbl, returns tbl
(put tbl :name "Bob") # adds key, returns tbl
tbl:count # => 1
tbl:name # => "Bob"
(del tbl :name) # removes key, returns tbl
Destructuring
(def {:name n :age a} {:name "Alice" :age 30})
n # => "Alice"
a # => 30
# & collects remaining keys
(def {:a va & rest} {:a 1 :b 2 :c 3})
rest # => {:b 2 :c 3}
See also
- arrays.md — array and @array operations
- sets.md — set operations
- destructuring.md — struct destructuring patterns
- types.md — mutability and type predicates