A named record state: the status that tags it and the properties that refine it.
A state module is a struct of witnesses, record plus whatever its
properties extract, and match/2 builds it or says why the record is not in
that state. A Hoare.Transition names states as its from and to.
defmodule Issued do
use Hoare.State, status: :issued, witnesses: [:lines], preloads: [:lines]
@impl Hoare.State
def properties, do: [&billable_lines/1]
defp billable_lines(%Issued{record: %{lines: []}}), do: {:error, :no_lines}
defp billable_lines(%Issued{record: %{lines: lines}} = state), do: {:ok, %{state | lines: lines}}
end
Hoare.State.match(Issued, invoice)
#=> {:ok, %Issued{record: invoice, lines: [...]}} | {:error, :not_issued | :no_lines}A property is an arrow over the state struct: it refines the tag, and may
fill a witness so nothing downstream has to look again. use is a
convenience over the behaviour; a module may implement the callbacks and
define the struct itself. defstate/3 declares several states in one module.
Summary
Callbacks
The reason reported when a record's status does not tag it.
What the record must have loaded for the properties to read it.
Arrows over the state struct that refine the tag and fill its witnesses.
The status value that tags a record as being in this state.
Functions
Declares the state from its options; properties/0 stays overridable.
Declares a state as a module nested in the caller, so one module holds a record's states and each stays a struct of its own.
Matches a sum of states: the first whose status tags the record decides.
The state's declared preloads; none when it declares none.
Types
Callbacks
@callback missing() :: reason()
The reason reported when a record's status does not tag it.
@callback preloads() :: [term()]
What the record must have loaded for the properties to read it.
@callback properties() :: [property()]
Arrows over the state struct that refine the tag and fill its witnesses.
@callback status() :: atom()
The status value that tags a record as being in this state.
Functions
Declares the state from its options; properties/0 stays overridable.
use Hoare.State, status: :issued, witnesses: [:lines], preloads: [:lines]:missing defaults to :not_<status>, downcased; :witnesses are the
struct's keys after record; :preloads defaults to none. The struct's
type is t/0.
Declares a state as a module nested in the caller, so one module holds a record's states and each stays a struct of its own.
defmodule Invoice.State do
import Hoare.State, only: [defstate: 2, defstate: 3]
defstate Draft, status: :draft
defstate Paid, status: :paid
defstate Issued, status: :issued, witnesses: [:lines], preloads: [:lines] do
@impl Hoare.State
def properties, do: [&billable_lines/1]
...
end
endThe states are Invoice.State.Draft and so on. Add import_deps: [:hoare]
to .formatter.exs to keep defstate free of parentheses.
Matches a sum of states: the first whose status tags the record decides.
When no state tags the record, the first state's missing/0 is the reason.
The state's declared preloads; none when it declares none.