Compound states contain child states. They model situations with meaningful internal structure: a
lamp can be On while its brightness is Dimmed, and both facts remain active together.
Give On a topology of its own.
The flat lamp can behave correctly, but it cannot express that both brightnesses are forms of being on. Nesting makes that domain fact structural.
1 · Flat topology
Flattening hides the shared truth.
Off, Dimmed, and Bright are mutually exclusive, yet both brightness states mean the lamp is on. SwitchOff is duplicated because that shared fact has no state of its own.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const SwitchOn = Schema.TaggedStruct("SwitchOn", {});
const SwitchOff = Schema.TaggedStruct("SwitchOff", {});
const Brighten = Schema.TaggedStruct("Brighten", {});
const Dim = Schema.TaggedStruct("Dim", {});
class Lamp extends State.Compound<Lamp>()("Lamp") {
static states = States.make(() => [Off, Dimmed, Bright]);
}
class Off extends State.Atomic<Off>()("Off") {
static transitions = Transitions.make(this, ({ on }) => [
on(SwitchOn, Dimmed),
]);
}
class Dimmed extends State.Atomic<Dimmed>()("Dimmed") {
static transitions = Transitions.make(this, ({ on }) => [
on(Brighten, Bright),
on(SwitchOff, Off),
]);
}
class Bright extends State.Atomic<Bright>()("Bright") {
static transitions = Transitions.make(this, ({ on }) => [
on(Dim, Dimmed),
on(SwitchOff, Off),
]);
}
export const LampChart = Statechart.make(Lamp);
2 · Compound state
Compound states make truths coactive.
On becomes compound, with Dimmed and Bright inside it. The active configuration can now say that the lamp is on and dimmed at the same time.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const SwitchOn = Schema.TaggedStruct("SwitchOn", {});
const SwitchOff = Schema.TaggedStruct("SwitchOff", {});
const Brighten = Schema.TaggedStruct("Brighten", {});
const Dim = Schema.TaggedStruct("Dim", {});
class Lamp extends State.Compound<Lamp>()("Lamp") {
static states = States.make(() => [Off, On]);
}
class Off extends State.Atomic<Off>()("Off") {
static transitions = Transitions.make(this, ({ on }) => [on(SwitchOn, On)]);
}
class On extends State.Compound<On>()("On") {
static states = States.make(() => [Dimmed, Bright]);
}
class Dimmed extends State.Atomic<Dimmed>()("Dimmed") {
static transitions = Transitions.make(this, ({ on }) => [
on(Brighten, Bright),
on(SwitchOff, Off),
]);
}
class Bright extends State.Atomic<Bright>()("Bright") {
static transitions = Transitions.make(this, ({ on }) => [
on(Dim, Dimmed),
on(SwitchOff, Off),
]);
}
export const LampChart = Statechart.make(Lamp);
3 · Shared ownership
Shared behavior belongs to the shared state.
Whenever either brightness is active, On is active too. One SwitchOff transition on On replaces both child copies and remains available throughout its subtree.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const SwitchOn = Schema.TaggedStruct("SwitchOn", {});
const SwitchOff = Schema.TaggedStruct("SwitchOff", {});
const Brighten = Schema.TaggedStruct("Brighten", {});
const Dim = Schema.TaggedStruct("Dim", {});
class Lamp extends State.Compound<Lamp>()("Lamp") {
static states = States.make(() => [Off, On]);
}
class Off extends State.Atomic<Off>()("Off") {
static transitions = Transitions.make(this, ({ on }) => [on(SwitchOn, On)]);
}
class On extends State.Compound<On>()("On") {
static states = States.make(() => [Dimmed, Bright]);
static transitions = Transitions.make(this, ({ on }) => [on(SwitchOff, Off)]);
}
class Dimmed extends State.Atomic<Dimmed>()("Dimmed") {
static transitions = Transitions.make(this, ({ on }) => [
on(Brighten, Bright),
]);
}
class Bright extends State.Atomic<Bright>()("Bright") {
static transitions = Transitions.make(this, ({ on }) => [on(Dim, Dimmed)]);
}
export const LampChart = Statechart.make(Lamp);
A configuration is a path
Lamp, On, and Dimmed are not competing phases. They are one active path through the topology,
from the broadest fact to the most specific: this lamp exists, it is on, and it is currently
dimmed.
Brighten replaces the active leaf, Dimmed, with its sibling Bright. It does not exit On, so
the shared fact remains active throughout the brightness change.
Entry follows an initial path
A compound state is not a place where entry can stop. Entering On must continue into one of its
children so the resulting configuration ends at an atomic leaf.
Direct-child order spells that initial choice. Because Dimmed appears first beneath On,
SwitchOn enters Lamp.On.Dimmed. Reversing the children would make Bright the initial
brightness; the choice is authored topology, not runtime guesswork.
Behavior belongs to its nearest owner
Transition selection begins at the active leaf and may continue through its active ancestors.
While either brightness child is active, the transition owned by On can accept SwitchOff.
This is more than deduplication. On is the domain fact that makes SwitchOff meaningful, so On
owns the rule. Every active descendant keeps that rule available because every one of them also
makes On active.
Test the boundary
- Reverse the child order beneath
On. Predict which brightness becomes active afterSwitchOnbefore running the chart. - Add a
Brokenstate. Decide first whether a broken lamp can still be on, then let that answer determine whetherBrokenbelongs insideOnor beside it. - Add
SetLowPower. If it must work from every brightness but not fromOff, choose the state that should own it before writing the transition.