Strange Loop Conference
Pure functional languages cannot support dynamically reconfigurable signal graphs without risking unbounded memory leaks from historical event look-backs, making static architectures necessary for deterministic time-travel debugging.
AI-assisted summary of "Controlling Time and Space: understanding the many formulations of FRP" by Evan Czaplicki — original source. Check the original for full context.
Source published:
Key ideas
- Evan Czaplicki distinguishes Elm's first-order FRP from higher-order FRP by its static signal graphs and infinite signals, which preserve temporal ordering and eliminate the need for historical event buffering. (6:17)
- Higher-order FRP introduces dynamic graph reconfiguration via nested signals (`Signal (Signal a)`), but referential transparency forces an 'infinite look-back' dilemma where past events must be retained, risking severe space leaks. (15:30)
- Asynchronous data flow libraries like Bacon.js and ReactiveX depart from classical FRP by abandoning static topologies, infinite lifespans, and synchrony in favor of finite streams and hot/cold subscription models. (20:53)
- Arrowized FRP (AFRP) solves memory retention issues during dynamic switching by decoupling automatons from the external world, freezing state safely when nodes are unobserved. (23:30)
Detailed notes
## Background Published on YouTube by the Strange Loop Conference on September 21, 2014, this talk features Evan Czaplicki, the creator of Elm and developer at Prezi. The presentation outlines the core concepts of Functional Reactive Programming (FRP), surveys its historical and theoretical variants, and explains how Elm's specific formulation enables features like hot-swapping and time-travel debugging.
## TL;DR Czaplicki demystifies the broad umbrella of Functional Reactive Programming by contrasting Elm's static, first-order signal graphs with higher-order, arrowized, and asynchronous dataflow variants. He demonstrates how Elm's constraints—static topologies, infinite signals, and synchronous defaults—enable reliable Model-View-Update architectures, seamless code hot swapping, and scrubbable time-travel debugging without memory-leaking look-back issues. The presentation then walks through the fundamental trade-offs of dynamic switching in higher-order FRP, finite event streams in libraries like Rx and Bacon.js, and decoupled automatons in Arrowized FRP. Ultimately, Czaplicki provides a pragmatic taxonomy, urging developers to choose reactive paradigms based on concrete application needs, ordering guarantees, and domain constraints rather than academic prestige.
## Atmosphere & style The presentation carries an engaging, accessible, and pragmatic academic tone, delivered with self-deprecating humor and candid clarity. Czaplicki speaks as a battle-tested functional programmer eager to spare his audience the years of conceptual confusion he endured, weaving effortlessly between high-level architectural philosophy and live visual demonstrations. Watching him casually scrub Mario backward across frame-by-frame keystrokes or laugh off accidentally creating 'Mega Mario' gives the session an exciting, workbench feel. It feels like an illuminating insider briefing that cuts through dense functional programming jargon to champion practical, understandable engineering.
## Bulletin - [06:17](https://www.youtube.com/watch?v=Agu6jipKfYw&t=377s) Evan Czaplicki distinguishes Elm's first-order FRP from higher-order FRP by its static signal graphs and infinite signals, which preserve temporal ordering and eliminate the need for historical event buffering. - [15:30](https://www.youtube.com/watch?v=Agu6jipKfYw&t=930s) Higher-order FRP introduces dynamic graph reconfiguration via nested signals (`Signal (Signal a)`), but referential transparency forces an 'infinite look-back' dilemma where past events must be retained, risking severe space leaks. - [20:53](https://www.youtube.com/watch?v=Agu6jipKfYw&t=1253s) Asynchronous data flow libraries like Bacon.js and ReactiveX depart from classical FRP by abandoning static topologies, infinite lifespans, and synchrony in favor of finite streams and hot/cold subscription models. - [23:30](https://www.youtube.com/watch?v=Agu6jipKfYw&t=1410s) Arrowized FRP (AFRP) solves memory retention issues during dynamic switching by decoupling automatons from the external world, freezing state safely when nodes are unobserved.
## Key takeaways - [00:01](https://www.youtube.com/watch?v=Agu6jipKfYw&t=1s) The term 'FRP' spans a diverse set of distinct paradigms, requiring explicit taxonomy across graph dynamism, signal lifetimes, and synchrony defaults. - [01:11](https://www.youtube.com/watch?v=Agu6jipKfYw&t=71s) First-order FRP in Elm uses four primitive operations to structure events without chaotic callbacks: input signals, `lift`, `foldp`, and signal combination (`merge`/`lift2`). - [06:17](https://www.youtube.com/watch?v=Agu6jipKfYw&t=377s) Elm's core design relies on four constraints: signals are tied to the physical world, signals are infinite, graphs are static, and event processing is synchronous by default. - [06:17](https://www.youtube.com/watch?v=Agu6jipKfYw&t=377s) The Model-View-Update pattern (the Elm Architecture) is a direct structural consequence of maintaining a static, event-driven signal graph. - [10:22](https://www.youtube.com/watch?v=Agu6jipKfYw&t=622s) A static signal graph enables reliable runtime code hot-swapping and deterministic time-travel debugging by replaying recorded input events over updated state step functions. - [13:00](https://www.youtube.com/watch?v=Agu6jipKfYw&t=780s) Higher-order FRP enables dynamic signal graphs using a `join` operation on signals of signals, but managing unobserved branches introduces substantial operational complexity. - [15:30](https://www.youtube.com/watch?v=Agu6jipKfYw&t=930s) Under equational reasoning and referential transparency, dynamic signal switching requires reconstructing cumulative history, causing an unbounded space leak without linear types or specialized type systems. - [20:53](https://www.youtube.com/watch?v=Agu6jipKfYw&t=1253s) Imperative reactive libraries (e.g., RxJS, Bacon.js) sidestep space leaks by discarding purity and structural sharing, instantiating fresh finite streams rather than tracking past identity. - [23:30](https://www.youtube.com/watch?v=Agu6jipKfYw&t=1410s) Arrowized FRP models computations as disconnected input-output state machines ('automatons'), enabling dynamic composition without continuous background churn or memory leaks. - [23:30](https://www.youtube.com/watch?v=Agu6jipKfYw&t=1410s) Because arrowized automata cannot read external hardware directly, AFRP serves primarily as a pure computation structuring tool rather than a comprehensive system I/O runtime. - [31:29](https://www.youtube.com/watch?v=Agu6jipKfYw&t=1889s) Architectural selection should be driven by deployment realities: asynchronous streams fit multi-client servers, whereas synchronous static graphs suit deterministic client-side UI tooling. - [34:01](https://www.youtube.com/watch?v=Agu6jipKfYw&t=2041s) Treating signals as effectful directly shifts an architecture out of pure FRP and into imperative asynchronous stream processing. - [36:39](https://www.youtube.com/watch?v=Agu6jipKfYw&t=2199s) UI component modularity in Elm is effectively achieved via hierarchical nesting of pure Model-Update-View functions and standard modules rather than complex dynamic reactive graphs. - [38:36](https://www.youtube.com/watch?v=Agu6jipKfYw&t=2316s) The push versus pull distinction in reactive UI is largely an implementation detail, as boundary-triggered push updates functionally mirror on-demand pull evaluation. - [38:36](https://www.youtube.com/watch?v=Agu6jipKfYw&t=2316s) Elm prioritizes human-driven graphical user interfaces, making complex backpressure and rate-limiting protocols unnecessary for its core design.
## Detailed walkthrough ### [00:01](https://www.youtube.com/watch?v=Agu6jipKfYw&t=1s) Demystifying Functional Reactive Programming
Evan Czaplicki, the creator of the Elm programming language, opens by stating that his talk is the distillation of years spent wrestling with Functional Reactive Programming (FRP) down into a single 40-minute session. His motivation is to spare others from having to endure that same painful journey of confusion. He jokes about the title, "Controlling Time and Space," admitting that at the time he submitted it, he felt like a Time Lord due to his work on time-travel debugging. However, the true core purpose of the talk is to dissect and categorize the wide diversity of concepts that travel under the umbrella label of "FRP."
To structure this exploration, Czaplicki establishes three overarching goals for the audience: 1. Understand what FRP actually is. 2. Categorize the diverse tools and paradigms that fall under that label. 3. Evaluate those variations systematically to understand their real trade-offs.
He outlines the roadmap: he will first introduce what he calls "first-order FRP"—which represents how Elm structures reactive systems—and then branch outward into other variations that build upon or alter this foundational model.
```mermaid flowchart LR accTitle: First-Order FRP Signal Graph Pipeline accDescr: Pipeline showing external user interactions transforming through a signal graph and outputting actions to the world.
subgraph Inputs["Inputs (World)"] A["User Events<br/>(Mouse, Keys)"] end
subgraph Graph["Signal Processing"] B["Signal Graph<br/>(Transform, Fold, Merge)"] end
subgraph Outputs["Outputs (World)"] C["Screen Render /<br/>HTTP Requests"] end
A --> B B --> C ```
[01:11](https://www.youtube.com/watch?v=Agu6jipKfYw&t=71s) The Mechanics of First-Order FRP in Elm
Czaplicki turns to the operational mechanics of first-order FRP, which mirrors the architecture of Elm. At its heart, the system is designed to handle interactions from the external world. A user sits at a computer typing and looking at the screen, producing mouse events and keystrokes. In typical JavaScript, this input lands in a chaotic tangle of ad-hoc callbacks. In Elm, inputs enter an explicitly structured processing network called a signal graph. As Czaplicki explains the motivation: “so sort of what FRP is about is rather than using callbacks and these kind of ad hoc ways of structuring time um we actually have a system that that gives it explicit structure that you can talk about in a in a more concise way”. The processed values are then communicated back to the platform to update the screen display or dispatch HTTP requests.
The signal graph is composed of four fundamental building blocks:
- **Inputs from the World:** Raw external inputs are modeled as signals. For example, `Mouse.position` does not have the type of a plain integer pair, but rather `Signal (Int, Int)`—a pair of integers changing over time. Similarly, `Keyboard.lastPress` tracks the most recent character pressed. Czaplicki humorously illustrates this with a trace of his typing: typing out `"hello strange Loop"`, then pausing to surf the web after getting distracted, and finally resuming typing later, showing how values evolve along a continuous timeline. - **Transformations (`lift`):** Raw signals can be modified using `lift`, which takes a pure mapping function `(a -> b)` and applies it across a `Signal a` to yield a `Signal b`. For instance, applying an `isConsonant` predicate across a keystroke signal converts the stream of characters (`'h'`, `'e'`, `'l'`, `'l'`, `'o'`) into a derived signal of booleans (`True`, `False`, `True`,...). - **Accumulating State (`foldp`):** State accumulation is handled by `foldp`, which stands for "fold from the past" (designed as the temporal analog to list operations `foldl` and `foldr`). Given an initial baseline state and an accumulator function, `foldp` updates the state on every incoming event. Czaplicki demonstrates counting keystrokes by feeding the key-press signal into `foldp`, stepping an integer counter forward (`1, 2, 3, 4, 5...`), noting that this same mechanism scales up to manage full application state, such as a complete Todo list. - **Combining Signals (`merge` and `lift2`..`liftN`):** Multiple streams can be unified using `merge` (which combines two signals of the same type) or combined via `lift2`, which applies a two-argument function across two concurrent signals whenever either updates (e.g., pairing discrete X and Y coordinate signals). Elm provides `lift` variants up to `lift8`, beyond which developers can combine higher-arity signals using standard functional patterns.
[06:17](https://www.youtube.com/watch?v=Agu6jipKfYw&t=377s) Core Design Principles and Architectural Payoffs
Moving beyond raw operational syntax, Czaplicki highlights the theoretical assumptions and constraints that define Elm's first-order FRP design:
- **Connected to the world:** Signal inputs directly represent concrete hardware or environmental realities like mouse coordinates and keyboard input. - **Infinite signals:** Signals cannot be arbitrarily created or destroyed at runtime. Czaplicki likens deleting an input signal to physically ripping the trackpad or mouse off a laptop mid-execution: inputs are fixed constants of the operating environment. - **Static signal graphs:** The topological shape of the signal network is fixed and fully known from the moment of application startup. Czaplicki notes that while dynamic topology sounds tempting, static topology yields massive guarantees and implementation benefits. - **Synchronous by default:** Branching paths in the graph must preserve temporal ordering. If a user types `"hello"`, the processing paths must preserve that sequencing through all intermediate downstream nodes without interleaving anomalies.
These design constraints directly enable two major benefits: high computational efficiency and a clean architectural separation. Efficiency is preserved because the system is purely event-driven; stateful nodes only need to inspect their single immediately preceding state value rather than maintaining historical buffers.
On the architectural front, the static graph structure directly gives birth to what is known as the Elm Architecture (Model-View-Update). Demonstrating a TodoMVC application integrated with browser `localStorage`, Czaplicki reveals the four clean structural parts of any standard Elm program:
1. **Model:** The explicit type definition of the entire application state (e.g., a list of tasks with description strings, editing flags, and completion states). 2. **Update:** A pure function stepping the model forward in response to user actions (such as adding or deleting tasks). 3. **View:** A pure, declarative transformation converting the current model into rendered HTML virtual DOM nodes, allowing developers to compose UI via standard functions like `List.map`. 4. **Inputs/Signals:** The pipeline feeding external user events into the update loop.
Czaplicki emphasizes that this clean separation is not an accidental style guide, but the direct consequence of the underlying FRP primitive choices: “that architecture that structure comes from the static signal graphs they guide you towards writing your applications that way”.
[10:22](https://www.youtube.com/watch?v=Agu6jipKfYw&t=622s) Hot Swapping and Time-Traveling in Elm
Evan Czaplicki demonstrates that beyond clean architecture, the major payoff of having a static signal graph is the ability to hot-swap code at runtime and travel through time while debugging. Demonstrating using the Mario program from the previous year, Czaplicki shows Mario standing on the screen. He adjusts the gravity constant in the source code, jumps Mario, and then changes gravity again to make it noticeably stronger. Because the signal graph is static and well-defined, these changes propagate straight into the running application without losing the current game state: “so we're able to change the behavior of the program while the program is running and sort of see those changes propagate uh automatically”.
```mermaid flowchart LR accTitle: Hot Swapping and Time Travel Debugger Workflow accDescr: Diagram showing how code edits in Sublime Text update the running Elm app and how event history allows scrubbable replays. subgraph Inputs ["Development & Events"] direction TB E["Editor (Sublime Text)"] H["User Inputs / Arrow Keys"] end subgraph Runtime ["Elm Static Signal Graph"] direction TB S["State (Model)"] U["Step Function (Update)"] D["Render (Display)"] end subgraph Debugger ["Time Travel Tooling"] direction TB T["Event History Log"] R["Scrubber / Slider"] end
E -->|"hot swap code"| U H -->|"record event"| T T -->|"replay events"| S R -->|"scrub to tick t"| S S --> U U --> D ```
Building on hot swapping, Czaplicki demonstrates a new version of the Elm Time Traveling Debugger, which he notes is about to be released in an easy-to-install package. Loading up the Mario game, he jokes about an unintended visual: "and we've got like Mega Mario ah man I changed the code by accident last night and forgot to change it back". With the debugger running, he pauses the program and scrubs the timeline backward toward the beginning. Instead of walking Mario left as he had done originally, he resumes playback and walks Mario to the right.
The debugger exposes the concrete stream of data driving the program at every tick: as Czaplicki steps backward frame by frame, the viewer can see the arrow keys being pressed (`right`, then `up-right`), and then released. Furthermore, hot swapping integrates directly with time travel: when Czaplicki opens Sublime Text, alters the gravity setting, and hits save, the updated jump logic re-runs across the entire recorded timeline of inputs. Mario suddenly leaps off the top of the screen because the accumulated jump velocity recalculates under the new rules, prompting Czaplicki to dial it down so Super Mario can have a reasonable jump height again.
- Hot swapping allows developers to adjust values such as physics constants while the program stays live and maintains its current state. - The Time Traveling Debugger captures the discrete history of external input events (like keyboard arrows), allowing scrub-and-replay from arbitrary points in time. - Changes made in an external editor like Sublime Text propagate not just into future frames, but across the entire logged history retroactively.
---
[13:00](https://www.youtube.com/watch?v=Agu6jipKfYw&t=780s) The Promise and Challenge of Higher-Order FRP
Czaplicki explains the fundamental architectural invariant that enables these developer tools: “a lot of these abilities to swap and code and Replay code comes from the fact that we know what if we know what the program looks like at the start we know what it's going to look like in an hour in a day um and we don't have to do some kind of crazy complex analysis to to figure that out or to match things up.” This stability is the defining characteristic of first-order Functional Reactive Programming (FRP).
However, functional programmers naturally ask what happens if signal graphs are allowed to reconfigure themselves dynamically at runtime. The mindset is often: "what is higher order FRP surely higher is better". As Czaplicki points out, exploring dynamic signal graphs has been a central research question in the field for years because it turns out to be surprisingly and deceptively difficult. He emphasizes that switching paradigms is never an objective upgrade; it is always an engineering trade-off where acquiring new expressive power requires giving up other properties.
In higher-order FRP, dynamic reconfiguration is traditionally achieved by introducing a `join` function that operates on a nested signal of signals (`Signal (Signal a)`). Conceptually, this can be visualized as having multiple incoming signals and using an outer signal to dynamically choose which inner signal to listen to at any given moment. But the moment dynamic switching is introduced, hard operational questions immediately emerge: What happens to the signals that are currently not being listened to? Should they keep running in the background, or should they be paused? If unobserved signals are introduced and accumulate over time, managing their state and lifecycle becomes significantly more complicated.
---
[15:30](https://www.youtube.com/watch?v=Agu6jipKfYw&t=930s) The Memory Problem: Infinite Look-Back and Equational Reasoning
To expose why higher-order FRP is so difficult, Czaplicki walks through a concrete example involving a mouse-click counter:
```mermaid flowchart TD accTitle: Higher Order FRP Join Dilemma accDescr: Flow diagram showing how switching between a click counter and a constant zero leads to equational reasoning requiring past event replay. subgraph StreamSwitch ["Selector Signal: Bool"] B{"Condition"} end subgraph Branches ["Signal Expressions"] C1["count Mouse.clicks"] C2["constant 0"] end subgraph Evaluation ["Join Evaluation"] J["join (Signal of Signals)"] Out["Current Count Output"] end
B -->|"True"| C1 B -->|"False"| C2 C1 --> J C2 --> J J --> Out ```
Consider a signal that counts mouse clicks, and another signal expression that switches between counting clicks and returning a constant zero based on a boolean value: - If the selector signal is `True`, it yields the click count. - If the selector signal is `False`, it yields `0`.
Czaplicki poses a timeline riddle: suppose the program begins with `True` and the user clicks 5 times, raising the counter to 5. The boolean signal is switched to `False`, resetting the output to 0. While it is set to `False`, the user clicks 5 more times. Finally, the selector is set back to `True`. What should the signal evaluate to? 1. Should it be `0` (restarting from scratch)? 2. Should it be `5` (resuming from where it left off)? 3. Should it be `10` (the cumulative count of all clicks over the application's entire run)?
Because this research originated in purely functional languages designed around equational reasoning and referential transparency, the choice is forced: identical expressions must behave identically and be substitutable. If `count Mouse.clicks` appears in the program, replacing it with a shared reference cannot alter its semantic meaning. Therefore, when the program switches back to `True`, the count must reflect the total history of clicks—in this case, 10.
This semantic guarantee leads directly to an operational disaster. If a user starts an application where a signal evaluates to `False` (emitting 0) for 20 minutes while clicking constantly, and then suddenly switches to an expression `count Mouse.clicks` that had never previously been evaluated, the runtime must still produce the full cumulative total. As Czaplicki highlights: “creating a new signal may need infinite look back I may need to look back at all the previous events to be able to reconstruct the signal so that I have these properties that I want”.
The consequence is a severe space leak: memory usage grows linearly with time because the system is forced to retain every historical event just in case an uninstantiated signal later needs to reconstruct its state from the beginning of time.
---
[18:01](https://www.youtube.com/watch?v=Agu6jipKfYw&t=1081s) Bounded Space Solutions and Architectural Realities
The community's response to this dilemma has not been to discard dynamic switching, but rather to use sophisticated type systems to rule out infinite look-back. Czaplicki points out that modern Haskell higher-order FRP libraries rely on clever type-level tricks to guarantee bounded memory usage.
A prime example is the academic paper titled *"Higher-Order Functional Reactive Programming in Bounded Space"*, on which Czaplicki quips: “the names get very long for these papers when your topic is functional reactor programming”. In that paper, the author uses linear types—a specialized type system virtually absent from mainstream functional languages like Haskell, OCaml, or Agda, with Idris only recently experimenting with similar concepts. By enforcing linear usage guarantees, the type system prevents the programmer from writing any signal transformation that demands retroactively looking back into historical events without an explicit programmer instruction to retain that data.
However, resolving the problem via complex types brings significant tradeoffs: - Signal graphs lose their static structure, making hot swapping and time-travel debugging substantially harder or impossible to implement cleanly. - The clean architectural patterns of simple FRP give way to complicated APIs and intimidating type machinery. - Even if the technical challenges were completely solved, the architectural value is dubious. Czaplicki revisits the TodoMVC application: if signals could be dynamically switched inside the state model, tasks might be represented as `List (Signal Task)` rather than a plain data `List Task`. Czaplicki expresses skepticism that managing collections of nested signals provides a cleaner design than updating a flat, immutable data structure, noting: “I'm not sure that's a better way um I think that's something that uh I'd like to see some examples and that could be that could be quite interesting but I'm not I'm not totally sold yet”.
Czaplicki then turns to the next major variant of reactive programming: asynchronous dataflow, exemplified by Reactive Extensions (Rx) and ReactiveCocoa.
[20:53](https://www.youtube.com/watch?v=Agu6jipKfYw&t=1253s) Asynchronous Data Flow in Imperative Languages: Bacon.js and Reactive Extensions
When reactive concepts appear in imperative environments—such as JavaScript libraries like Bacon.js or ReactiveX (RxJS)—they represent a distinct departure from classical functional reactive programming. Evan Czaplicki points out that whenever you encounter something marketed as FRP in an imperative language, it almost certainly belongs to this asynchronous data-flow category. In contrast to strict, pure formulations, these libraries discard several foundational constraints from the original design checklist. The assumption that signal graphs are static is immediately eliminated, along with the assumption that signals are infinite. In this paradigm, signals act as finite event streams: a stream can spin up, emit four events, and terminate. Furthermore, synchronous execution by default is abandoned in favor of asynchronous dispatch. While developers can still orchestrate synchrony manually, asynchrony is baked in as the default operating model.
```mermaid flowchart LR accTitle: Imperative Signal Lifecycle and Cold Versus Hot Streams accDescr: Demonstrates how imperative reactive streams are instantiated anew, execute asynchronously, and behave based on hot or cold subscriptions. subgraph S1["Stream Creation"] A["Event Source"] --> B["New Signal Instance"] end subgraph S2["Evaluation Modes"] B --> C{"Subscriber Attached?"} C -->|"Hot Signal"| D["Emits Events Continuously"] C -->|"Cold Signal"| E["Stays Dormant Until Listened To"] end subgraph S3["Disposal"] D --> F["Terminates / GC Cleans Up"] E --> F end ```
These systems handle higher-order streams using an operation commonly called `flatten`, which merges streams of streams. Czaplicki observes with a grin that imperative languages benefit from naming things intuitively for everyday programmers. However, because purity and referential transparency are not enforced, behavioral semantics change drastically. In an earlier pure example, defining an expression like `count mouseClicks` assumed consistent identity across references. In an imperative runtime, no such equivalence exists. Czaplicki explains the practical reality: “Now we're in an imperative language and no one ever said you were going to have that guarantee so whenever you create one of these signals you're creating a totally new one.” If a toggle alternates between counting clicks and returning zero, switching to false resets the count; switching back to true instantiates an entirely fresh signal starting from zero, while runtime garbage collection cleans up unreferenced nodes. This design introduces the puzzle of unobserved signals, leading libraries like Reactive Extensions to divide streams into "hot" and "cold" variants: a hot signal continues pushing values regardless of whether anyone is listening, whereas a cold signal halts until an observer subscribes.
- Static signal graphs are crossed off; graphs are dynamically constructed and torn down at runtime. - Infinite signals are replaced by finite event streams that complete or die after emitting values. - Asynchrony replaces synchronous execution as the system default. - Structural sharing is abandoned: instantiating a stream produces a distinct runtime instance without referential equivalence. - Reactive Extensions introduces hot signals (which run and produce events unprompted) and cold signals (which wait for active subscribers).
---
[23:30](https://www.youtube.com/watch?v=Agu6jipKfYw&t=1410s) Arrowized FRP: Decoupling Computations from the World with Automatons
Arrowized Functional Reactive Programming (AFRP) emerged in Haskell with libraries such as Yampa and later Netwire, while Elm explored the same paradigm via its `Automaton` library. Like higher-order FRP, arrowized FRP eliminates the constraint that signal graphs must be static. However, it introduces a radically different philosophical trade-off: it crosses off the rule that signal graphs must be directly connected to the outside world. Instead of dealing with ambient, globally wired signals, arrowized FRP models stateful computation through isolated step functions known as automatons. Czaplicki describes an automaton metaphorically as a tiny self-contained robot that accepts an input on one side and produces a transformed output on the other side.
```mermaid flowchart TD accTitle: Automaton Composition and State Retention accDescr: Shows pure and stateful automatons connected in sequence, with dormant automatons safely preserving state when switched out of the active data path. subgraph Active["Active Execution Path"] IN["Input Event"] --> P1["Pure Automaton (+1)"] P1 --> P2["Pure Automaton (+1)"] P2 --> S1["Active Stateful Automaton (Count: 5)"] S1 --> OUT["Output Result"] end subgraph Inactive["Dormant / Disconnected Automatons"] S2["Green Automaton (Count: 3)"] S3["Blue Automaton (Count: 0)"] end ```
The basic API in Elm's `Automaton` library illustrates this cleanly: - `pure`: Wraps a pure function into an automaton (for instance, an incrementer where passing 1 outputs 2, 42 yields 43, and 100 returns 101). - `state`: Embeds persistent internal state similar to `foldp`. An automaton initialized with a base state of 0 updates its internal accumulator upon receiving incoming inputs (e.g., tallying strings like `"hello"` and `"how are you"` into sequential counts). - Chaining (`>>>`): Connects two automatons end-to-end, feeding the output of one into the input of the next, such as piping two `+1` stages together to form a `+2` pipeline.
The brilliance of this architecture becomes obvious during dynamic switching. Czaplicki walks through a scenario with three separate counting automatons initialized to zero. When the first automaton is placed in the active position, it processes five events and its state reaches five. It is then switched out for a second automaton, which processes three events. Because the disconnected nodes are completely severed from the external world, they freeze their internal state without leaking memory or continuing to churn cycles. Czaplicki highlights this core advantage: “What's really clever here is that these nodes aren't connected to the world so when we take it out of the graph this question of what happens does it keep running it just answered uh it doesn't keep running it's not connected to anything.” The severe drawback, however, is that an automaton cannot directly reference external hardware events like mouse coordinates; values must be manually sampled and fed into it. Consequently, arrowized FRP functions as a code structuring tool—competing directly with pure functions and module systems—rather than a comprehensive I/O runtime, though it has found notable success in domain-specific tasks such as audio processing and game loops.
---
[27:59](https://www.youtube.com/watch?v=Agu6jipKfYw&t=1679s) Mapping the Design Space: Static Graphs, Finite Streams, and Pragmatic Architecture
Synthesizing these competing formulations, Czaplicki emphasizes that different FRP implementations are not in hostile competition; rather, they occupy complementary niches within a broader design landscape. In his overview diagram, Elm is positioned at the intersection of these styles because it strategically adopts features from each. Higher-order dynamic FRP adds capabilities, but Czaplicki deliberately avoids introducing it into Elm's core to preserve Elm's strong static guarantees—though he remains open to it if someone designs a formulation that retains those guarantees. Conversely, asynchronous data flow libraries like Bacon.js or Rx do not belong inside Elm; instead, developers should embed Elm inside them. At Prezi (Czaplicki's employer at the time), engineers can write a dedicated interactive UI component in Elm with full compiler guarantees and expose it outwards to integrate into an enclosing Bacon.js or Rx application architecture. Meanwhile, arrowized FRP requires no runtime compromises at all, living comfortably as a userland Elm library.
| FRP Formulation | Signal Graph Topology | Signal Lifespan / Nature | Relationship to External World / Elm | Primary Ecosystem Examples | |:--- |:--- |:--- |:--- |:--- | | **First-Order FRP (Elm Core)** | Static (fixed at compile time) | Infinite signals | Synchronous by default, globally wired to events | Elm | | **Higher-Order FRP** | Dynamic (graphs alter at runtime) | Infinite signals | Synchronously evaluates nested signals | Classical Haskell FRP papers | | **Asynchronous Data Flow** | Dynamic | Finite event streams (can end) | Imperative, asynchronous; wraps Elm externally | Bacon.js, ReactiveX (RxJS) | | **Arrowized FRP** | Dynamic switching allowed | Explicit signals omitted | Disconnected from the world; functions as an internal library | Yampa, Netwire, Elm `Automaton` |
To navigate this taxonomy, Czaplicki points developers to two fundamental architectural questions. First, does the system distinguish between static graphs (which remain rigid and analytically tractable) and dynamic graphs (which mutate at runtime)? Second, within dynamic graphs, how are signals modeled: are they infinite (common in Haskell), finite streams (common in imperative environments), or omitted entirely via decoupled arrows?
As a warning chime plays—prompting him to joke that he is running low on time—Czaplicki underscores the practical questions practitioners must ask when choosing an event architecture. Developers must verify whether a library is synchronous by default so input ordering is preserved without race conditions. Czaplicki emphasizes this guarantee: “When it comes to categorizing the questions you want to be asking are is it synchronous by default right am I getting these ordering guarantees where I know that if someone types hello and clicks a button that we're going to get it in that order and not the button click happens halfway through cuz the processing happens too takes too long or something.” Finally, systems must permit controlled asynchrony when necessary: while synchrony is the ideal default for local state, tasks like HTTP requests and disk writes must run asynchronously, a balance that Elm deliberately bakes into its foundational design.
- Detailed formal history and literature citations covering first-order through higher-order FRP can be found in Evan Czaplicki's master's thesis (*Concurrent FRP for GUIs*). - Architectural selection hinges on whether an application requires strict ordering guarantees (synchronous default) or background side-effect delegation (asynchronous escape hatches).
[31:29](https://www.youtube.com/watch?v=Agu6jipKfYw&t=1889s) Evaluating FRP Formulations by Application Needs
When assessing different Functional Reactive Programming formulations, Evan Czaplicki emphasizes that not all existing systems have an explicit or well-defined plan for how asynchrony should behave—a critical detail developers must scrutinize, particularly when exploring higher-order FRP. Programmers must ask concrete architectural questions: can the system talk directly about inputs? Is it structured as an arrowized system, or is it primarily a mechanism for routing events? Can the underlying signal graph be reconfigured dynamically at runtime? Czaplicki cautions against choosing a technology based merely on impressive academic vocabulary or library marketing, arguing instead that practical project requirements must dictate the choice.
Asynchronous data flow systems like Reactive Extensions (Rx) shine on the server, where ten users might connect simultaneously requesting data; having asynchronous behavior by default prevents any single client from blocking others based on arrival order. Conversely, if an application requires global synchrony or relies on the deterministic, replayable time-travel debugging tools Elm provides, an asynchronous-by-default architecture is a poor fit. Czaplicki points out that debugging dynamic graph implementations is notoriously elusive, warning developers that choosing a paradigm is a long-term commitment: “this isn't just a cool thing to start with you have to carry this through for the whole life of your application and finally does the code come out nice”. While dynamic and asynchronous models offer broader feature sets, they introduce significant cognitive overhead, making it crucial to weigh whether added complexity is truly justified over simpler alternatives. Czaplicki closes his presentation by suggesting that mastering the foundational concepts in Elm makes it significantly easier to navigate other FRP paradigms across the broader programming landscape.
- **Asynchrony design**: Not all higher-order FRP libraries provide a coherent model for asynchronous execution. - **Server vs. Client trade-offs**: Rx-style asynchronous data flow excels when handling concurrent client connections on a server, whereas Elm's synchronous signal graph enables deterministic time-travel debugging. - **Complexity cost**: Dynamic graph reconfiguration provides power at the expense of difficult debugging and long-term code maintainability.
---
[34:01](https://www.youtube.com/watch?v=Agu6jipKfYw&t=2041s) Q&A: Referential Transparency, Effectful Signals, and Automata
Opening the audience Q&A, an attendee asks about the `join` operator in higher-order FRP and the challenge of referential transparency. The attendee wonders why systems do not simply treat signals as inherently effectful rather than attempting to look backward in time across all past events to satisfy purity constraints: “is there a reason you don't just model signals as effectful then done with it”. Czaplicki explains that treating signals as effectful moves the system directly into the realm of asynchronous data flow libraries like Reactive Extensions or Bacon.js. While higher-order FRP libraries historically enforced the invariant that identical signal definitions should evaluate to identical historical values, relinquishing that constraint is a deliberate design trade-off that abandons referential transparency in exchange for pragmatic event streaming.
```mermaid flowchart LR accTitle: Stepping an Automaton in Arrowized FRP accDescr: Diagram showing how an input value and current automaton produce an output value and a new automaton instance. subgraph Step["Automaton Step Execution"] In["Input Value"] AutoCurr["Automaton (State N)"] In --> AutoCurr AutoCurr --> Out["Output Value"] AutoCurr --> AutoNext["Next Automaton (State N+1)"] end ```
Another audience member asks about arrowized FRP and how dynamic signal switching behaves once signals are wired up in a running Elm application. Czaplicki explains that in an arrowized FRP model, dynamism is built directly into the structure of automata before they are connected to runtime inputs. Stepping an automaton does not mutate an in-place graph; rather, it pure-functionally transitions the state forward: “so when you step in aaton forward what you're really getting is it the value it produces and the new automaton that you should run”. Czaplicki likens this process to forking reality: the caller receives both the current output value and a brand-new automaton configured to process future values, preserving past history without unexpected runtime coupling.
- **Effectful signals**: Treating streams as side-effecting is the core distinction separating FRP from libraries like Bacon.js and Rx. - **Automata stepping**: Arrowized systems evaluate by taking an input and returning a pair consisting of the output value and the updated automaton. - **Dynamic switching**: Arrowized automata encapsulate state transitions functionally rather than relying on global dynamic graph mutations.
---
[36:39](https://www.youtube.com/watch?v=Agu6jipKfYw&t=2199s) Q&A: Component Modularity and the Nested Elm Architecture
An attendee asks about achieving component modularity within an Elm program, using the canonical TodoMVC application as an example: how would one extract the single-line task editor component and reuse it across another application? Czaplicki acknowledges that while higher-order or arrowized FRP can provide component abstraction by wrapping dynamic subgraphs, his current advocacy in Elm centers on standard functional building blocks: “What I my opinion is that this is something we should Explore More in Elm what I'm advoc right now... is sticking with module system and functions”.
Instead of relying on complex reactive abstractions, Czaplicki recommends structuring applications hierarchically using modules that expose a Model, an update function, and a view function. To modularize individual tasks, a developer writes a self-contained task module defining its model, update logic, and view rendering. The higher-level to-do list module then embeds the task's model, update, and view within its own architecture. Czaplicki admits that while higher-order reactive components remain an open area of inquiry, the functional module-nesting pattern offers clarity, noting that “it's not clear to me that one way is going to be superior to the other and I think there hasn't been enough exploration to know like this is the right way or even if there is a right way for all cases yeah”. An attendee briefly asks whether Czaplicki has reviewed another reactive framework from literature, but Czaplicki notes that he is unfamiliar with it.
- **Modularity debate**: Component encapsulation can be pursued either through higher-order/arrowized FRP constructs or through purely functional Model-Update-View architectures. - **Nested Elm Architecture**: Hierarchical reuse is achieved by nesting a child module's state, update function, and view function within a parent module. - **Ecosystem maturity**: Czaplicki highlights that UI modularity patterns in functional languages are still an active area of experimentation without a single settled standard.
---
[38:36](https://www.youtube.com/watch?v=Agu6jipKfYw&t=2316s) Q&A: Push vs. Pull Semantics and Backpressure in UI Systems
Addressing a question about how the classic "push versus pull" distinction maps onto his FRP taxonomy, Czaplicki remarks that the terminology often creates unnecessary confusion. While the asynchronous data flow community often speaks of "hot versus cold" observables to describe behavior when observers are absent, push versus pull traditionally refers to the direction of data flow evaluation. Elm employs an event-driven push architecture: incoming user interactions immediately push updates downward through the signal graph. In contrast, pull systems initiate evaluation from the display output upward, pulling and sampling values from upstream nodes at specific timestamps.
Czaplicki argues that the theoretical divide between push and pull is largely overstated in practice: “the argument we've made in in the work we've done on FRP is that uh a push system can model a pull one in that when you get to the bottom of your program one way to say it is I pull the values through another way to say it is I trigger a push through”. Because triggering a push from the boundary produces an identical result to pulling on demand, the practical differences are minimal.
A final question asks how FRP graphs handle producers that emit data faster than the system can process—commonly known as backpressure. Czaplicki explains that Elm is intentionally tailored for user interfaces, where inputs originate from human beings clicking or typing:
> “in the domain of um UI uh you're dealing with human input so you're not going to be getting in that situation very often so a lot of these systems are prioritized towards use cases like that”
Because human interaction speeds rarely overwhelm the evaluation loop, built-in backpressure mechanisms have not been a primary architectural concern for Elm. Noting that he has run slightly over his allotted time, Czaplicki wraps up the session and invites attendees to continue the discussion outside.
- **Push vs. pull equivalence**: Push systems propagate events downstream from inputs, while pull systems sample upstream on demand; functionally, a pull request can be modeled as triggering a push update. - **Hot vs. cold distinction**: In asynchronous data flow, "hot" streams emit regardless of subscribers, whereas "cold" streams generate items only upon active subscription. - **Backpressure in Elm**: Because Elm focuses primarily on graphical user interfaces driven by human input rates, complex backpressure and rate-limiting pushback mechanisms are not prioritized in its core design.
## Notable quotes > [01:11](https://www.youtube.com/watch?v=Agu6jipKfYw&t=71s) what FRP is about is rather than using callbacks and these kind of ad hoc ways of structuring time um we actually have a system that that gives it explicit structure that you can talk about in a in a more concise way — Evan Czaplicki
## Mentioned Evan Czaplicki, Elm, Prezi, Strange Loop Conference, TodoMVC, Sublime Text, Mario, Haskell, OCaml, Agda, Idris, Bacon.js, Reactive Extensions (RxJS), ReactiveCocoa, Yampa, Netwire
## Video description Functional Reactive Programming (FRP) is becoming an increasingly common way to structure event- driven code, but the term "FRP" actually refers to a large body of increasingly diverse work. This talk will quickly cover the basics of FRP, and then go into a couple different formulations of FRP that people are beginning to use. We will explore how these formulations fit together historically and theoretically. Finally, we will see how the particular variation of FRP used in Elm makes it easy to implement hot- swapping and Elm's Time Traveling Debugger.
by Evan Czaplicki (@czaplic) - Prezi
I am a functional programmer at heart, and my broad goal is to provide the tools and learning resources to make functional languages common in industry. I develop Elm 's compiler, tools, and community to make it great for projects within Prezi and in industry in general.
[Source](https://www.youtube.com/watch?v=Agu6jipKfYw)