Parts I–III treated strings over an alphabet and machines that accept or reject them. Part IV made the machine a transducer: each input symbol updates state and can emit output. Part V covered several machines that share resources (Petri nets).
Those parts assume an input sequence is available. This part is how a program delivers symbols and how consumers keep state after a prefix.
Question. Given an event sequence $e_1 e_2 e_3 \ldots$ over $\Sigma$ (button presses, UserRegistered, socket frames): how do consumers see those symbols, and what state do they keep?
| Mechanism | Delivery | Relation to automata |
|---|---|---|
| Observer | in-process notify on each occurrence |
broadcast the next input symbol |
| FRP | compose Event and Behavior |
transducer over the event string; Behavior ≈ state / Moore output after the prefix |
| Event log | append symbols to a durable ordered log | store the string; many machines replay prefixes |
Same $\Sigma$ and the same “state after a prefix.” The difference is whether the string is discarded after notify, folded into a Behavior, or stored for replay.
Content:
- Delivery after transducers and Petri nets
- Observer: in-process notify (GoF / Smalltalk)
- FRP: event-driven pain →
Event+Behavior→ combinators (Haskell) - Transactions; Rx as cousin; React mapping
- Unified event log: durable string, many readers
- Cheat sheet and self-check
Why this belongs in the series
In Part I a language is a set of strings over $\Sigma$. A click counter can use $\Sigma = {\mathsf{click}}$. A registration pipeline can use $\Sigma = {\mathsf{UserRegistered}, \mathsf{PaymentFailed}, \ldots}$. The event history is one concrete string.
A DFA, Moore machine, or Petri-net marking answers: after the prefix seen so far, what is the current configuration? Running software still needs:
- emit the next symbol,
- let zero or more consumers react,
- optionally store the string so a late or restarted consumer can catch up.
Observer, FRP, and event logs are three ways to do that.
Observer pattern
Simplest delivery: do not store the string. On each new symbol, call the registered listeners.
The name most people meet first is from Gamma, Helm, Johnson, and Vlissides (Design Patterns: Elements of Reusable Object-Oriented Software, 1994), often called the Gang of Four (GoF). Their intent line is: define a one-to-many dependency so that when one object changes state, all dependents are notified and updated automatically.
The idea is older than that book. Smalltalk-80’s dependents mechanism (model broadcasts changed / changed:; views and controllers respond with update / update:) is the usual source the GoF catalog points at. Krasner and Pope documented that MVC / dependents loop in 1988. The same four authors also listed Observer in their earlier ECOOP’93 paper before the book’s full write-up.
Where you already see it
| Setting | Subject (source) | Observers (dependents) |
|---|---|---|
| Spreadsheet | a cell’s value | formulas that read it; charts bound to the range |
| IDE / editor | document buffer | syntax highlighter, outline, status bar, dirty-flag |
| Stock / metrics dashboard | latest quote or gauge reading | table row, sparkline, alert rule |
| GUI toolkit | button / toggle / model object | labels, loggers, analytics sinks |
Spreadsheet → chart. Edit a table cell. Dependent formulas recompute. A chart subscribed to that range redraws. The cell store does not import the chart library; it notifies whoever registered.
Components
| Component | Role |
|---|---|
| Subject (observable) | holds state or is the event source |
| Observer interface | e.g. update(...) / onEvent(...) |
| Concrete observers | views, loggers, analytics sinks |
| subscribe / attach | register an observer with the subject |
| unsubscribe / detach | remove it from the list |
| notify | subject calls each registered observer |
The subject stores a list of observers. On a change, it calls notify. It does not hard-code each concrete observer class.
Toy GUI
- Subject:
Toggle - Event: switched on/off
- Observers: (1) update a status label, (2) write a log line, (3) refresh a chart
Flow: user flips the toggle → Toggle.notify() → each subscribed object runs its handler. The Toggle class does not import the chart implementation.
Why this structure is used
- Loose coupling: the subject depends on an observer interface, not on each listener’s class.
- Many listeners: one event, $N$ reactions.
- Dynamic registration: subscribe when a panel opens; unsubscribe when it closes.
- Separation of concerns: reaction logic lives in observers.
Automata reading
Treat the subject’s event type as a symbol from $\Sigma$. Each notify feeds that symbol to every attached consumer. Each concrete observer updates its own state (and may emit UI). The subject does not keep the full input string. After notify returns, that occurrence is gone unless an observer stored it. FRP Behaviors and event logs address that gap.
FRP: Event and Behavior
Observer answers who to call when the next symbol arrives. It does not treat time-varying values as data you can compose.
FRP (functional reactive programming) = programming with values that vary over time, as first-class entities. The early formulation is Elliott and Hudak, Functional Reactive Animation (Fran, ICFP 1997), in Haskell. Later teaching libraries in the same model: Yampa, Reactive Banana, Sodium.
Two primitives (Fran said behaviour; most Haskell libraries say Behavior):
| Type | Meaning |
|---|---|
Event a |
discrete occurrences over time; each carries a value of type a; no value between occurrences |
Behavior a |
a value of type a defined at every time $t$; always has a current sample |
A mouse-move burst is an Event. The current mouse $x$ is a Behavior Double. A button press is an Event (). Spreadsheet cell A1 is a Behavior; finishing an edit is closer to an Event that updates it.
Litmus test. If the library has a first-class value that exists at every moment (not only when something emits), it is closer to classic FRP. If everything is a stream of discrete emissions, it is Rx-style reactive programming (cousin, not the same model). Starting from RxJS / RxDart and calling that “FRP” skips the Behavior half.
In series terms: an Event sequence is a string over $\Sigma$; a Behavior is state (or Moore output) after the prefix so far. Combinators are how you write the transducer without a hand-rolled observer list.
Baseline: plain event-driven (the pain)
Callbacks + mutable locals. Mouse $x$ and click:
var mouseX = 0
onMouseMove(x) { mouseX = x }
onClick() { drawAt(mouseX) }
Questions this raises:
- Who owns
mouseX? - What if move and click are ordered differently than you assumed?
- How do you combine mouse position with a held key without more shared mutables?
- How do you unsubscribe without leaking handlers?
Event-driven code is actions that run when something happens. State is outside the handlers and coordinated by hand. That is Observer / DOM addEventListener territory.
The FRP shift
Instead of “on event, mutate state,” you define time-varying values as functions of other time-varying values.
-- Classic FRP types (Fran / Reactive Banana / Sodium style)
-- Event a -- discrete occurrences
-- Behavior a -- continuous current value
mouseMove :: Event Double -- x on each move
click :: Event ()
mouseX :: Behavior Double
mouseX = hold 0 mouseMove -- Event → Behavior (initial 0)
-- sample Behavior at the instant Event fires (Sodium: snapshot; often called tag)
clickPos :: Event Double
clickPos = tag mouseX click
tag (also snapshot) is the operation Rx does not make first-class: read the current Behavior when an Event occurs. Between clicks, clickPos has no value; mouseX always does.
Core combinators (minimal set)
On Event: map (transform payload), filter, merge, tag / snapshot (sample a Behavior), accumE / fold into a new Event of running state.
On Behavior: map, apply / combine several Behaviors, hold (Event + initial → Behavior), accumB (fold Events into a Behavior).
Running example (click positions, then a list of them):
mouseMove :: Event Double
click :: Event ()
mouseX :: Behavior Double
mouseX = hold 0 mouseMove
clickPos :: Event Double
clickPos = tag mouseX click
-- Behavior of all click x positions so far (newest at the front)
points :: Behavior [Double]
points = accumB [] ((:) <$> clickPos)
The UI consumes points (draw the list). It is not the coordinator of onMouseMove / onClick mutables. State is derived.
Toy: click counter (map, hold, tag)
Same idea over $\Sigma = {\mathsf{click}}$. Count clicks; Behavior holds the total (Moore-style “output = current state”).
clicks :: Event ()
deltas :: Event Int
deltas = map (const 1) clicks -- payload 1 per click; map does not accumulate
-- sample current count at each delta (Sodium snapshot / classic tag)
updates :: Event Int
updates = (\c d -> c + d) <$> snapshot count deltas
count :: Behavior Int
count = hold 0 updates
label :: Behavior String
label = map show count
Pipeline:
clicks --map(const 1)--> deltas --snapshot(+)--> updates
↑ current
[ hold ] <--------- updates
| signal | over time |
|---|---|
| clicks | • • • |
deltas after map |
1 1 1 |
count after hold |
0 | 1 | 2 | 3 |
| updates | 1 2 3 |
| primitive | job |
|---|---|
map |
click → delta $1$ (no accumulation) |
hold |
current count (Behavior) |
tag / snapshot |
newCount = oldCount + 1 at event time |
map alone is not a counter. Accumulation needs hold plus sampling the current Behavior.
Missed first event vs Behavior
- An Event fires before a listener subscribes.
- The listener attaches later.
- That past occurrence is not delivered again.
Example: config finished loading at startup; a widget that subscribes afterwards never hears "loaded".
A Behavior (UI people often say cell) always has a current value. A late reader samples it now. Correctness does not require having been subscribed at the first change. “FRP is asynchronous” does not state this distinction; the Event / Behavior split does.
Spreadsheet as Behaviors
| Spreadsheet | FRP |
|---|---|
A1 holds 10 |
Behavior with current 10 |
| user commits a new number | Event that updates the cell |
B1 = A1 * 2 |
map / arithmetic on a Behavior |
chart on B1 |
another reader of that Behavior |
Transactions (why purists separate FRP from Rx)
Classic FRP updates are transactional: occurrences at the same logical instant are processed together; you do not observe a half-updated intermediate Behavior. Sodium is built around that. Rx Observable chains fire sequentially by default; simultaneous logical events are not one transaction unless you build that yourself.
Rx after the model (cousin, not the core)
Only after Event + Behavior is clear:
| Classic FRP | Rx (RxJS / RxDart / …) | |
|---|---|---|
Event a |
Observable<a> (roughly) |
|
Behavior a |
no true continuous Behavior; BehaviorSubject remembers last emission |
|
accumB / fold |
scan |
|
tag / snapshot |
not primitive; sample by hand | |
| focus | values over time + sampling | async IO, debounce, switchMap, cancellation |
Rx is a popular discrete reactive stream library inspired by FRP ideas. It is not classic FRP. Same warning for “FRP” marketing around UI state libraries that only expose streams.
Boundaries (short)
| Model | Core objects |
|---|---|
| Event-driven / Observer | callbacks on discrete occurrences |
| Classic FRP | Event + Behavior, sampling, often transactional updates |
| Rx-style | Observable streams only |
| Signal / fine-grained reactivity (MobX, Solid, Vue) | dependency-tracked cells; not classic FRP, not Rx |
React is a render layer that can consume state. It is not an FRP runtime.
In React (mapping, not identity)
I use React because the mapping is short; Next.js uses the same client hooks.
| Classic FRP | React-ish counterpart |
|---|---|
Behavior (current value) |
useState / props present on first render |
Event |
onClick, onChange, socket push |
hold + tag / fold |
setCount((c) => c + 1) |
| subscribe / unsubscribe | useEffect + cleanup |
| late joiner reads Behavior | child mounts and reads current count |
function ClickCounter() {
const [count, setCount] = useState(0); // Behavior-like current total
function handleClick() {
// Event: one click; fold into the Behavior
setCount((c) => c + 1);
}
return <button onClick={handleClick}>{count}</button>;
}
External pushes still look like Observer:
useEffect(() => {
function onMessage(e) {
setLatest(JSON.parse(e.data));
}
socket.addEventListener("message", onMessage);
return () => socket.removeEventListener("message", onMessage);
}, [socket]);
If a message arrived before this effect ran, this listener missed that Event. Keep the latest payload in state (a Behavior / cell) if late UI must show current data.
React does not provide hold / tag as named primitives. The split still holds: Events are occurrences; state is the current Behavior you can read after you arrive.
Observer delivered each click by callback. FRP named the Event and the derived Behavior. Neither stores the full click string. An event log does: append the symbols, then let consumers read them.
Unified event log vs tight observers
Tight observers (recap)
Producers call consumers in-process (notify). Low latency. The subject holds references to listeners. The input string is usually not retained. After notify, history is gone unless something else stored it. Replay needs an explicit log.
Unified event log
Facts are appended to an ordered log. The log is the string $e_1 e_2 e_3 \ldots$ over the domain alphabet. Independent consumers read it on their own schedule (Kafka-style systems are the usual example). Each consumer is a transducer / state machine: offset = how much of the string it has processed; local tables = state after that prefix.
Part I: a language is a set of strings; here the system emits one concrete string (the log). Part V: several machines share a concurrent world; here they share an append-only history.
Toy. Service writes UserRegistered records to a log ($\Sigma$ includes that symbol).
- Email service reads the log and sends a welcome message.
- Billing reads the same log and creates an account record.
- Analytics reads the log for funnels.
The registration service does not import those three codebases. A reader can restart from an earlier offset and re-run on a prefix of the stored string.
Benefits
| Benefit | Meaning | Automata reading |
|---|---|---|
| Decoupling | writers need not know readers | producer only appends to the string |
| Replay | rebuild state from history | re-run the machine on a prefix |
| Audit | durable record of what occurred | the stored string is the record |
| Multiple consumers | add a reader without changing the writer | many transducers, one input string |
| Temporal decoupling | a consumer can catch up after downtime | late joiner processes missed symbols |
One cost
Operational complexity, eventual consistency between readers, or retention / storage growth of the log.
Comparison
| Where the string lives | What a consumer keeps | Late joiner | |
|---|---|---|---|
| Observer | not retained (push only) | whatever the handler stored | misses past symbols unless state was added |
| FRP Behavior | usually not retained; Behavior holds current state after the prefix | the Behavior | sees current state; not every past payload unless logged |
| Event log | durable ordered log | offset + derived state | replays symbols from the log |
FRP makes state after the prefix explicit (the Behavior). It does not require storing every symbol for every consumer.
Cheat sheet
Core event string over Σ → deliver symbols → keep / derive state
Observer Subject, subscribe/unsubscribe, notify, concrete observers
in-process broadcast of the next symbol; string not retained
GoF one-to-many notify on state change (1994 book; ECOOP’93 catalog)
Source idea Smalltalk dependents / MVC (Krasner & Pope 1988)
FRP time-varying values as first-class data (Fran / Haskell)
Event a discrete occurrences; no value between them
Behavior a value at every time t (Fran: behaviour); always sampleable
hold Event + initial → Behavior
tag/snapshot sample Behavior at Event time
map transform Event payload or Behavior value (no accumulation)
accumB fold Events into a Behavior
vs Rx Observable ≈ Event only; not classic Behavior + tag
vs Observer Observer = who to call; FRP = how Event/Behavior are derived
React useState ≈ Behavior; onClick ≈ Event; not an FRP runtime
Event log durable ordered string; offset = how much prefix consumed
log vs notify durable log + many readers vs in-process callbacks
Self-check
- In one line: what shared question ties Observer, FRP, and event logs to Parts I–V?
- Name the six Observer pieces from memory.
- Give one production example (subject + at least one observer).
- State the FRP axiom and name the two classic primitives.
- What does
tag/snapshotdo that plainmapon Events does not? - Give a one-line “missed first event” failure and the Behavior-based fix.
- Why is “RxJS is FRP” the wrong teaching start?
- In React terms: what is closer to a Behavior, and what is closer to an Event?
- Does
map (const 1)alone implement a counter? - Map each mechanism to “where the event string lives.”
- State two benefits of a unified log over
notify, and one downside.
Answers
1. How software delivers symbols of an event sequence (a string over $\Sigma$) to consumers and keeps state after a prefix.
2. Subject, Observer interface, concrete observers, subscribe, unsubscribe, notify.
3. e.g. spreadsheet cell → dependent formulas and chart; or document buffer → highlighter and outline.
4. FRP = program with time-varying values as first-class data. Primitives: Event a, Behavior a.
5. Samples a Behavior at the instant an Event fires (payload from continuous state, not only from the Event).
6. Failure: subscribe after “config loaded”; never hear it. Fix: hold config in a Behavior; read current value on join.
7. Rx exposes stream/Observable ≈ Event only; classic FRP also has continuous Behavior and first-class sampling (tag / snapshot).
8. Behavior: useState / current props. Event: click / message (or any push you listen for).
9. No. It only produces deltas. Accumulation needs hold plus snapshot / tag (or accumB).
10. Observer: not retained (push only). FRP Behavior: current state after the prefix. Event log: durable ordered log.
11. Benefits: any two of decoupling, replay, audit, independent consumers, temporal decoupling. Downside: e.g. ops complexity, eventual consistency, or log retention cost.
Series map
| Part | Topic |
|---|---|
| I | languages, grammars, accepters (Linz Ch 1) |
| II | DFA, NFA, subset construction (Linz Ch 2) |
| III | regex, ε-NFA, lazy remove ε (Linz Ch 3) |
| IV | Moore / Mealy transducers (Linz App A) |
| V | Petri nets, reachability, safeness |
| VI (this post) | delivering events: Observer, classic FRP, logs |
References
- Gamma, E., Helm, R., Johnson, R., and Vlissides, J. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994. (Observer / “Gang of Four.” No free full PDF hosted here.)
- Gamma, E., Helm, R., Johnson, R., and Vlissides, J. Design Patterns: Abstraction and Reuse of Object-Oriented Design. In ECOOP’93, LNCS 707, Springer, 1993, pp. 406–431. (Earlier catalog entry that includes Observer.)
Read in BetterReads - Krasner, G. E. and Pope, S. T. A Description of the Model-View-Controller User Interface Paradigm in the Smalltalk-80 System. Journal of Object-Oriented Programming, 1(3):26–49, 1988. (Dependents / MVC change notification.)
Read in BetterReads - Elliott, C. and Hudak, P. Functional Reactive Animation. In Proceedings of ICFP 1997. (Fran: behaviours and events in Haskell.)
Read in BetterReads
Author PDF: conal.net - Teaching libraries in the same classic model: Reactive Banana (Haskell), Sodium (several hosts), Yampa (Haskell arrowized FRP). RxJS / RxDart are discrete-stream cousins, not drop-in FRP.
- Part I–Part V.