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