Hoare.Transition (hoare v0.1.0)

Copy Markdown View Source

A declared transition: the states it leaves, the state it reaches, the guards that must hold and the effects it performs, each list composed as one Kleisli arrow. {from, guards} body {to} is the Hoare triple; run/4 discharges it.

from and to are Hoare.State modules. The context is any map or struct with a record key, the record whose status moves, and a state key, which check/2 fills by matching from. Each arrow is ctx -> {:ok, ctx} | {:error, reason} and may refine the context it returns. The domain declares the transition; the module owning the records supplies the body of the commit when it runs it, and commit/5 wraps that body in the store's locked transaction, the from re-check and the to write.

The law: a run ends in to or leaves the record in from, never between. A bare effect must be idempotent, so a commit that fails after it is recovered by running again. An effect paired with an undo is reverted, newest first, when anything after it fails.

Summary

Functions

Matches from into the context's state, then runs the guards.

Commits in one locked transaction: re-reads the record, confirms it is still tagged by a from state, runs body, writes to's status and asserts to.

Runs the transition end to end: guards, then effects, then the commit.

Types

arrow()

@type arrow() :: (ctx() -> {:ok, ctx()} | {:error, term()})

body()

@type body() :: (ctx() -> {:ok, term()} | {:error, term()})

ctx()

@type ctx() :: %{
  :record => Hoare.Store.subject(),
  :state => struct() | nil,
  optional(atom()) => term()
}

effect()

@type effect() :: arrow() | {arrow(), undo()}

opts()

@type opts() :: [store: module(), lock: term()]

t()

@type t() :: %Hoare.Transition{
  effects: [effect()],
  from: [module(), ...],
  guards: [arrow()],
  to: module()
}

undo()

@type undo() :: (ctx() -> :ok)

Functions

check(transition, ctx)

@spec check(t(), ctx()) :: {:ok, ctx()} | {:error, term()}

Matches from into the context's state, then runs the guards.

commit(transition, ctx, body, converges?, opts)

@spec commit(t(), ctx(), body(), boolean(), opts()) ::
  {:ok, Hoare.Store.subject()} | {:error, :not_found | :status_changed | term()}

Commits in one locked transaction: re-reads the record, confirms it is still tagged by a from state, runs body, writes to's status and asserts to.

A record already tagged by to is a concurrent run of the same transition: it commits nothing and is {:ok, record} when converges? (no effect left anything behind), {:error, :status_changed} otherwise. A violated to raises, rolling the transaction back. The re-read carries no preloads, so only the tag is re-checked; the properties held on the context under the guards. The status is written through the schema's changeset/2.

run(transition, ctx, body, opts)

@spec run(t(), ctx(), body(), opts()) :: {:ok, ctx()} | {:error, term()}

Runs the transition end to end: guards, then effects, then the commit.

body is the domain's own writes; commit/5 wraps them. On success the context comes back with record replaced by the committed record. opts needs :store, a Hoare.Store; :lock defaults to {schema, id}.