State and Lifecycle Protocols
State is not one magical object with one opaque lifecycle.
Hara separates the abilities involved in a live value: reading it, replacing it, applying a conditional update, observing changes, waiting with a timeout, realising deferred work, composing a settled result, resuming a computation, and closing a resource.
That separation is useful to a builder. Code can depend on exactly the ability it needs, and a new value can provide that ability without adopting an entire framework lifecycle.
01 — Reading and replacing are separate abilities
Section titled “01 — Reading and replacing are separate abilities”An atom supports IDeref for reading and IReset for complete replacement.
The two operations are deliberately different contracts.
(let [cell (atom {:status :idle})
before (std.protocol.ideref/deref cell)]
(std.protocol.ireset/reset cell {:status :running})
{:before before
:after (std.protocol.ideref/deref cell)})A function that only needs to inspect a value can depend on IDeref without
being given the power to replace it.
Builder’s rule: separate observation from authority.
02 — Conditional replacement is another contract
Section titled “02 — Conditional replacement is another contract”ICas performs a replacement only while the current value still matches an
expected value.
(let [cell (atom 10)
first (std.protocol.icas/cas cell 10 11)
second (std.protocol.icas/cas cell 10 12)]
{:first first
:second second
:current (std.protocol.ideref/deref cell)})The first update succeeds. The second observes that 10 is no longer current
and leaves the cell at 11.
Builder’s rule: make the condition for a state transition visible.
03 — Observation is not mutation
Section titled “03 — Observation is not mutation”IWatch lets another part of the system observe a change without becoming the
owner of the state.
(let [cell (atom 10)
seen (atom nil)]
(std.protocol.iwatch/watch-add
cell
:audit
(fn [key reference old-value new-value]
(std.protocol.ireset/reset
seen
{:key key :old old-value :new new-value})))
(std.protocol.ireset/reset cell 11)
(std.protocol.iwatch/watch-remove cell :audit)
(std.protocol.ireset/reset cell 12)
{:seen (std.protocol.ideref/deref seen)
:remaining (count (std.protocol.iwatch/watch-list cell))})The audit watch observes the transition from 10 to 11. Removing it means
the later replacement does not change the recorded observation.
04 — A timeout is part of the caller’s policy
Section titled “04 — A timeout is part of the caller’s policy”IDerefTimeout makes bounded waiting explicit. A value can define what it
returns when the caller’s time budget is unavailable.
(do
(defstruct TimedValue [value])
(extend-type TimedValue std.protocol.idereftimeout/IDerefTimeout
(deref-timeout [timed milliseconds timeout-value]
(if (neg? milliseconds)
timeout-value
(field timed :value))))
(let [timed (TimedValue 42)]
{:available
(std.protocol.idereftimeout/deref-timeout timed 100 :timed-out)
:expired
(std.protocol.idereftimeout/deref-timeout timed -1 :timed-out)}))The contract does not force every deferred value to use the same scheduler or host implementation. It fixes the program-facing question.
05 — Realisation is visible state
Section titled “05 — Realisation is visible state”IRealize separates asking whether work is complete from causing the value to
be produced.
(do
(defstruct DeferredReading [value realised])
(extend-type DeferredReading std.protocol.irealize/IRealize
(realized? [reading]
(std.protocol.ideref/deref (field reading :realised)))
(realize [reading]
(do
(std.protocol.ireset/reset (field reading :realised) true)
(field reading :value))))
(let [reading (DeferredReading 42 (atom false))
before (std.protocol.irealize/realized? reading)
value (std.protocol.irealize/realize reading)]
{:before before
:value value
:after (std.protocol.irealize/realized? reading)}))A builder can expose lazy work without hiding whether that boundary has been crossed.
06 — A promise is a settlement contract
Section titled “06 — A promise is a settlement contract”IPromise names the operations available on a result that may settle. The
provider can be a browser worker, native runtime, host future, or a value built
for deterministic testing.
(do
(defstruct SettledValue [value state])
(extend-type SettledValue std.protocol.ipromise/IPromise
(state [settled]
(std.protocol.ideref/deref (field settled :state)))
(value [settled]
(field settled :value))
(then [settled function]
(function (field settled :value)))
(catch [settled function]
settled)
(finally [settled function]
(do (function) settled))
(cancel [settled]
(do
(std.protocol.ireset/reset (field settled :state) :cancelled)
settled)))
(let [settled (SettledValue 21 (atom :fulfilled))
doubled (std.protocol.ipromise/then settled (fn [value] (* value 2)))
before (std.protocol.ipromise/state settled)]
(std.protocol.ipromise/finally settled (fn [] nil))
(std.protocol.ipromise/cancel settled)
{:before before
:value (std.protocol.ipromise/value settled)
:then doubled
:after (std.protocol.ipromise/state settled)}))The example is synchronous by design. It demonstrates the contract without requiring nondeterministic timing.
07 — Resumption is distinct from settlement
Section titled “07 — Resumption is distinct from settlement”A coroutine describes where a computation can pause and continue. Its protocol therefore exposes status and resume rather than promise chaining.
(do
(defstruct Stepper [status])
(extend-type Stepper std.protocol.icoroutine/ICoroutine
(status [stepper]
(std.protocol.ideref/deref (field stepper :status)))
(resume [stepper input]
(if (= input :stop)
(do
(std.protocol.ireset/reset (field stepper :status) :dead)
:stopped)
(do
(std.protocol.ireset/reset (field stepper :status) :suspended)
{:yield input}))))
(let [stepper (Stepper (atom :suspended))
first (std.protocol.icoroutine/resume stepper :next)
stopped (std.protocol.icoroutine/resume stepper :stop)]
{:first first
:stopped stopped
:status (std.protocol.icoroutine/status stepper)}))Promises model when a result settles. Coroutines model where work resumes. Hara keeps those jobs separate.
08 — Resource closure is a first-class ability
Section titled “08 — Resource closure is a first-class ability”IClose gives files, connections, sessions, iterators, and domain resources a
shared release boundary.
(do
(defstruct ManagedHandle [closed])
(extend-type ManagedHandle std.protocol.iclose/IClose
(close [handle]
(do
(std.protocol.ireset/reset (field handle :closed) true)
nil)))
(let [handle (ManagedHandle (atom false))
before (std.protocol.ideref/deref (field handle :closed))]
(std.protocol.iclose/close handle)
{:before before
:after (std.protocol.ideref/deref (field handle :closed))}))Closure is not an incidental method hidden on a concrete provider. It is a contract that callers can invoke deliberately and tools can inspect.
The builder’s state vocabulary
Section titled “The builder’s state vocabulary”| Question | Protocol |
|---|---|
| What is current now? | IDeref |
| Replace it completely? | IReset |
| Replace it only if unchanged? | ICas |
| Observe later replacements? | IWatch |
| Wait within a time budget? | IDerefTimeout |
| Has deferred work been produced? | IRealize |
| How does a result settle and compose? | IPromise |
| Where can a computation resume? | ICoroutine |
| How is a resource released? | IClose |
Continue through the Protocol Atlas →{ .md-button .md-button—primary } Return to Protocols for Builders →{ .md-button }