The Elm architecture — design
Class:
design· Function axis:f0(the loop) +f1trait ·f2bridge macro ·f3harness. Strict: this file states only the shape we run. Every why, every alternative, lives inelm-reasoning.md.
f0 — the loop (the whole)
A facett component is a value that owns all of its observable state and mutates it in exactly one place. The loop:
view(&self, ui)paints from&selfand returnsVec<Msg>— the events the frame produced. It takes&self: it cannot mutate. (facett-core/src/elm.rs:77)update(&mut self, msg)is the only&mut selfmethod. It applies oneMsgand returnsVec<Effect>. (facett-core/src/elm.rs:71)- The
Facet::uibridge closes the loop:for m in self.view(ui) { self.update(m) }. It is written for you by the macro (f2), never by hand.
Four laws (the "FC" contract) make the loop hold; a component is conformant iff all hold:
| law | statement | enforced by |
| FC-1/3 | Model = all state you could observe or assert on |
type Model is the serialized truth (elm.rs:56) |
| FC-2 | every input is a Msg; the writable surface is the Msg enum, nothing else |
update is the sole &mut (elm.rs:71) |
| FC-4/9 | view is pure — same &self → same paint + same Msgs, no mutation |
view(&self, …) signature (elm.rs:77) |
| FC-5 | selections/handles are stable ids, not indices | Model fields hold ids |
| FC-7 | ephemeral caches (layout, animation) live in Cell/RefCell outside Model |
not serialized; excluded from state() |
| FC-8 | Effect is data; a component that does no I/O uses enum Effect {} (uninhabited) |
type Effect (elm.rs:61) |
Reuse map (what the Elm subsystem uses): it depends only on egui (paint surface), serde (Model ↔ json), and facett's own Facet trait. It is used by every facett component crate (36 impl Elm today) and, downstream, by nornir's test-matrix and robot functions through f3 — see nornir test-function design.
f1 — the Elm trait
pub trait Elm (facett-core/src/elm.rs:54). Five members, nothing more:
type Model: Serialize + DeserializeOwned + Clone + PartialEq; // :56 the observable truth
type Msg: Clone + Debug; // :59 the input alphabet
type Effect; // :61 side-work as data
fn title(&self) -> &str; // :64
fn state(&self) -> &Self::Model; // :67 read-only view of Model
fn update(&mut self, msg: Self::Msg) -> Vec<Self::Effect>; // :71 the only mutation
fn view(&self, ui: &mut egui::Ui) -> Vec<Self::Msg>; // :77 pure paint → Msgs
Model: PartialEq is what makes snapshots diffable; DeserializeOwned is what lets the harness rehydrate a component from json.
f2 — the bridge macro impl_facet_via_elm!
macro_rules! impl_facet_via_elm (facett-core/src/elm.rs:131) writes impl Facet from an impl Elm: the title, the view→update ui loop, and a serde state_json. Three forms:
| form | invocation | writes |
| 1 | impl_facet_via_elm!(MyView) (elm.rs:132) |
bridge + default state_json = serde(state()) |
| 2 | impl_facet_via_elm!(MyView, { …overrides… }) (elm.rs:136) |
bridge + default state_json + extra Facet methods (must not redefine state_json) |
| 3 | impl_facet_via_elm!(MyView, custom_state_json, { fn state_json… }) (elm.rs:162) |
bridge with the default state_json suppressed, so the block supplies a richer one |
f3 — the headless harness
facett-core/src/harness.rs drives a component with no window, no GPU:
| fn | signature | use |
drive |
drive<C: Elm>(&mut C, msgs) -> Vec<C::Effect> (harness.rs:219) |
apply a Msg script, collect Effects |
snapshot |
snapshot<C: Elm>(&mut C, msgs) -> C::Model (:229) |
drive, then return the resulting Model |
snapshot_json |
… -> serde_json::Value (:237) |
the Model as json (golden-file friendly) |
state_json |
state_json<C: Elm>(&C) -> Value (:244) |
current Model as json, no drive |
render_sized / headless_render / render_themed[_sized] |
(&mut dyn Facet, …) -> RenderReport (:71,:76,:81,:92) |
paint once into an off-screen surface; RenderReport::drew() (:20) asserts it painted |
This is the seam every automated consumer uses: a Msg script in, a Model/RenderReport out, deterministically. Robot testing and the test-matrix are exactly this seam at scale.