05. Array and object¶
Vectors and maps are persistent values. array and object create mutable marker values.
The constructor makes the boundary explicit:
[]creates a persistent vector.{}creates a persistent map.arraycreates a mutable indexed value.objectcreates a mutable string-keyed value.
Learning goals¶
By the end of this lesson, you can:
- Create mutable arrays and objects.
- Read and update them with restricted dot calls.
- Explain their identity semantics.
- Clone before independent mutation.
- Convert data at a persistent or mutable boundary.
- Choose a persistent collection by default.
Create an array¶
Use array for mutable indexed storage:
(def buffer
(array "alpha" "beta" "gamma"))
Read an item with a dot call:
(. buffer (get 1))
; => "beta"
The dot form is restricted to declared marker methods. It is not general host reflection.
Update an array¶
Set an indexed item:
(. buffer (set 1 "BETA"))
Read it again:
(. buffer (get 1))
; => "BETA"
The same array identity now exposes the new value.
This differs from assoc on a vector:
(def values ["alpha" "beta" "gamma"])
(def changed (assoc values 1 "BETA"))
values and changed are separate persistent vectors.
Add and remove array items¶
Append an item:
(. buffer (push-last "delta"))
Prepend an item:
(. buffer (push-first "zero"))
Remove the final item:
(. buffer (pop-last))
Remove the first item:
(. buffer (pop-first))
The operations mutate the array and return according to the array method contract.
Inspect the array after each operation. Do not infer a persistent result from the method name.
Insert, remove, and slice¶
Insert at an index:
(. buffer (insert 1 "inserted"))
Remove at an index:
(. buffer (remove 1))
Create a slice:
(def section
(. buffer (slice 0 2)))
The slice allocates separate array storage for the selected range.
Use clone when you need a complete independent copy:
(def copied-buffer
(. buffer (clone)))
Mutating copied-buffer does not mutate buffer.
Array transformations¶
Arrays support restricted transformation methods:
(. (array 1 2 3)
(map (fn [number] (* number 2))))
Filter array items:
(. (array 1 2 3 4)
(filter even?))
Fold left:
(. (array 10 20 12)
(fold-left + 0))
; => 42
These are array methods. They are distinct from Hara's ordinary persistent and lazy collection functions.
Arrays participate in selected protocols¶
Marker arrays support declared collection protocols such as count and indexed access:
(count buffer)
(nth buffer 0)
Protocol support does not make the array persistent. A later mutation remains visible through the same identity.
Create an object¶
Use object for mutable string-keyed storage:
(def record
(object
"line" "alpha"
"number" 1
"valid" true))
Object keys are strings.
Read a key:
(. record (get "line"))
; => "alpha"
Check a key:
(. record (has? "number"))
; => true
Update an object¶
Set a key:
(. record (set "line" "ALPHA"))
Delete a key:
(. record (delete "valid"))
Inspect keys and values:
(. record (keys))
(. record (vals))
(. record (pairs))
The exact returned collection family follows the object method contract. Treat the object itself as mutable identity.
Assign several fields¶
Use assign to copy string-keyed fields from another compatible object value:
(def extra
(object
"status" "ready"
"source" "input.txt"))
(. record (assign extra))
Inspect record after the call.
Use clone before assignment when the original object must remain independent:
(def independent
(. record (clone)))
(. independent (set "status" "changed"))
Identity semantics¶
Persistent values compare by their data contract. Mutable marker values have identity semantics.
Two objects with the same fields are still two mutable identities:
(def first-record (object "value" 42))
(def second-record (object "value" 42))
Do not use a mutable marker as a stable map key based only on its current contents.
A mutation can change its visible data after another component receives it.
Keep mutation local¶
A good mutable boundary has one clear owner:
(defn collect-three [source]
(let [output (array)]
(loop [remaining 3]
(if (and (> remaining 0)
(iter-has? source))
(do
(. output (push-last (iter-next source)))
(recur (- remaining 1)))
output))))
The function creates and owns the array. It returns the array only after it finishes the local mutation sequence.
For general application data, a persistent vector often produces simpler code.
Convert at the boundary¶
Convert a mutable array into a persistent vector when downstream code should not observe later mutation.
One direct approach is to stream its items into an eager transform:
(mapv identity buffer)
Convert a persistent vector into an array when a mutable API requires one:
(def mutable-values
(apply array [1 2 3]))
Make the conversion visible near the boundary.
For objects, copy required string-keyed fields into a persistent map:
{:line/text (. record (get "line"))
:line/number (. record (get "number"))}
This gives domain code stable keyword keys and persistent update semantics.
Choose the collection from ownership¶
Use a persistent vector or map when:
- several parts of the program share the value;
- old versions remain useful;
- equality should depend on contents;
- state transitions should be easy to test;
- a render or trace may retain the value.
Use an array or object when:
- a host or extension API expects mutable identity;
- a local algorithm needs a mutable work buffer;
- the operation has one clear owner;
- you can convert back at the boundary;
- mutation is part of the public contract.
Do not choose mutation only to make an update expression shorter.
Build the course batch buffer¶
Create a mutable buffer for a small output batch:
(defn make-batch []
(array))
(defn add-to-batch! [batch line]
(. batch (push-last line))
batch)
(defn batch-full? [batch limit]
(>= (count batch) limit))
Use it:
(def batch (make-batch))
(add-to-batch! batch "alpha")
(add-to-batch! batch "beta")
(batch-full? batch 2)
; => true
Convert it before storing it in persistent application state:
(def batch-value
(mapv identity batch))
The atom can now store a stable vector snapshot:
(swap! run assoc :run/current-batch batch-value)
Practice loop¶
- Create one vector and one array with the same items.
- Update each at index
0. - Inspect the original vector.
- Inspect the original array binding.
- Clone the array and mutate the clone.
- Explain which values share identity.
Repeat the exercise with a map and object.
Common mistakes¶
Expecting set to return a new array¶
The method mutates the existing marker value.
Using keyword keys in an object¶
Object keys are strings. Use a persistent map when keyword keys are the correct domain representation.
Sharing a mutable marker without an owner¶
Document which component can mutate it and when mutation ends.
Assuming dot calls expose the host¶
Dot calls are restricted to marker methods. Hara does not grant general host reflection through ..
Keeping mutable data in long-lived history¶
Convert to a persistent value before storing snapshots, traces, or render models.
Check yourself¶
You are ready for the next lesson when you can answer these questions:
- What does
arraymake explicit? - How does
assocdiffer from arrayset? - Why does an object use string keys?
- What does identity semantics mean for mutation?
- When should you clone a marker value?
- Why should mutation have one clear owner?
- When should you convert back to a persistent value?
Continue with 06. Bytes and strings.