Collection Protocols
Collections are where Hara’s protocol model becomes concrete.
A vector, map, set, list, iterator, or value you define yourself does not need to inherit one universal collection class. It provides the particular abilities that make sense for it: counting, lookup, positional access, persistent update, traversal, or reduction.
This lab uses the canonical method Vars under std.protocol.*. Ordinary names
such as count, get, and assoc are the convenient public surface; the
protocol methods reveal the contracts underneath them.
01 — One operation, one ability
Section titled “01 — One operation, one ability”count is the ordinary language operation. std.protocol.icount/count is the
canonical protocol method it can dispatch through.
{:ordinary (count [:a :b :c])
:protocol (std.protocol.icount/count [:a :b :c])}The two calls agree because the vector provides ICount.
Builder’s rule: depend on the smallest ability that expresses the question.
02 — Lookup is not presence
Section titled “02 — Lookup is not presence”A lookup asks for a value. A find asks whether an entry exists. Those questions
must remain distinct when a present key is associated with nil.
(let [record {:present nil}]
{:lookup (std.protocol.ilookup/lookup record :present)
:found (std.protocol.ifind/find record :present)
:present? (IFind/has? record :present)
:missing? (IFind/has? record :missing)})get alone cannot distinguish the last two cases. IFind preserves the
information needed to do so.
Builder’s rule: choose a contract that preserves the distinction your system needs.
03 — Position and key are different contracts
Section titled “03 — Position and key are different contracts”Indexed access and keyed lookup may both return a value, but they describe different structures.
{:position (std.protocol.inth/nth [:north :east :south] 1)
:key (std.protocol.ilookup/lookup {:east 90 :south 180} :east)}INth says a value has positions. ILookup says a value accepts keys. A type
may provide either contract, both, or neither.
04 — Update without losing the previous value
Section titled “04 — Update without losing the previous value”Persistent update is represented by separate association and removal contracts.
(let [original {:name "Nova" :score 10}
changed (std.protocol.iassoc/assoc original :score 35)
trimmed (std.protocol.idissoc/dissoc changed :name)]
{:original original
:changed changed
:trimmed trimmed})All three values remain available. The protocol says what the operation means; the concrete collection preserves its own representation rules.
05 — Direction matters
Section titled “05 — Direction matters”Adding at a collection’s natural edge and prepending a value are separate operations.
{:conj (std.protocol.iconj/conj [2 3] 4)
:cons (std.protocol.icons/cons [2 3] 1)}The direct protocol calls place the receiver first. The ordinary public cons
form keeps its documented (cons item collection) order.
Builder’s rule: do not hide direction or ownership inside a generic “append” operation.
06 — Traversal is a resource
Section titled “06 — Traversal is a resource”IIter acquires a cursor. IIterator advances that cursor. The collection and
the traversal are not the same value.
(let [cursor (std.protocol.iiter/iter [:a :b])]
[(std.protocol.iiterator/iter-next? cursor)
(std.protocol.iiterator/iter-next cursor)
(std.protocol.iiterator/iter-next cursor)])The first check observes the next item without logically consuming it. The two
iter-next calls then return :a and :b.
Builder’s rule: make one-shot work visible instead of pretending every source is replayable.
07 — Let a new value join an existing vocabulary
Section titled “07 — Let a new value join an existing vocabulary”A new value can become countable without changing count or creating another
calling convention.
(do
(defstruct CountedShelf [items])
(extend-type CountedShelf std.protocol.icount/ICount
(count [shelf]
(count (field shelf :items))))
(count (CountedShelf ["hammer" "saw" "plane"])))The extension belongs with CountedShelf. Every existing caller of count
can use it immediately.
08 — Compose a useful value from several contracts
Section titled “08 — Compose a useful value from several contracts”A domain value can provide only the collection abilities that make sense for its job.
(do
(defstruct Catalog [entries])
(extend-type Catalog std.protocol.icount/ICount
(count [catalog]
(count (field catalog :entries))))
(extend-type Catalog std.protocol.ilookup/ILookup
(lookup [catalog key]
(get (field catalog :entries) key)))
(extend-type Catalog std.protocol.ifind/IFind
(find [catalog key]
(IFind/find (field catalog :entries) key)))
(let [catalog
(Catalog {:saw {:stock 4}
:plane {:stock 2}})]
{:count (count catalog)
:saw (get catalog :saw)
:plane? (IFind/has? catalog :plane)
:drill? (IFind/has? catalog :drill)}))Catalog is now countable, lookupable, and searchable. It is not required to
pretend to be a vector, map, or host object.
That is the protocol advantage: a stable vocabulary can accept new values without central coordination or framework-specific adapters.
What to carry into a real system
Section titled “What to carry into a real system”| Question | Protocol |
|---|---|
| How many values are present? | ICount |
| What value belongs to this key? | ILookup |
| Does this entry actually exist? | IFind |
| What occupies this position? | INth |
| What is the persistent replacement? | IAssoc, IDissoc |
| How is a value added? | IConj, ICons |
| How is a traversal acquired and advanced? | IIter, IIterator |
Continue through the Protocol Atlas →{ .md-button .md-button—primary } Return to Protocols for Builders →{ .md-button }