1001 — Event Loop
Tutorial
The Problem
Implement a functional event loop that dispatches Event variants to pure state transitions. The dispatch function takes the current AppState and an Event, returning the next AppState. Run the loop with fold over a vector of events, stopping at Quit. Compare with OCaml's recursive run_event_loop using a typed handler record.
🎯 Learning Outcomes
Click { x, y }, KeyPress(char))dispatch(state, event) -> AppState as a pure functionAppState { clicks: state.clicks + 1, ..state } for partial updatesfold to thread state through a sequence of eventsVecDeque for a mutable event queue with O(1) push/poploop state eventsCode Example
#![allow(clippy::all)]
// 1001: Simple Event Loop
// Poll events, dispatch enum handlers, accumulate state
use std::collections::VecDeque;
// --- Event enum ---
#[derive(Debug, Clone, PartialEq)]
enum Event {
Click { x: i32, y: i32 },
KeyPress(char),
Timer(String),
NetworkData(String),
Quit,
}
// --- Application state ---
#[derive(Debug, Clone, PartialEq)]
struct AppState {
clicks: u32,
keys: String,
timers: u32,
network_msgs: Vec<String>,
}
impl AppState {
fn new() -> Self {
AppState {
clicks: 0,
keys: String::new(),
timers: 0,
network_msgs: Vec::new(),
}
}
}
// --- Pure functional dispatch: one event → next state ---
fn dispatch(state: AppState, event: &Event) -> AppState {
match event {
Event::Click { .. } => AppState {
clicks: state.clicks + 1,
..state
},
Event::KeyPress(c) => AppState {
keys: format!("{}{}", state.keys, c),
..state
},
Event::Timer(_) => AppState {
timers: state.timers + 1,
..state
},
Event::NetworkData(msg) => {
let mut msgs = state.network_msgs.clone();
msgs.push(msg.clone());
AppState {
network_msgs: msgs,
..state
}
}
Event::Quit => state, // handled by loop
}
}
// --- Approach 1: Functional event loop over a Vec ---
fn run_event_loop(events: Vec<Event>, init: AppState) -> AppState {
events.iter().fold(init, |state, event| {
if event == &Event::Quit {
state
}
// stop processing new events via fold
else {
dispatch(state, event)
}
})
}
// Better version that actually stops at Quit:
fn run_until_quit(events: Vec<Event>, mut state: AppState) -> AppState {
for event in events {
match event {
Event::Quit => break,
e => state = dispatch(state, &e),
}
}
state
}
// --- Approach 2: Event loop with a queue (mutable, real-world style) ---
struct EventLoop {
queue: VecDeque<Event>,
state: AppState,
}
impl EventLoop {
fn new(state: AppState) -> Self {
EventLoop {
queue: VecDeque::new(),
state,
}
}
fn push(&mut self, event: Event) {
self.queue.push_back(event);
}
fn push_many(&mut self, events: Vec<Event>) {
for e in events {
self.queue.push_back(e);
}
}
fn run(&mut self) {
while let Some(event) = self.queue.pop_front() {
match event {
Event::Quit => break,
e => self.state = dispatch(self.state.clone(), &e),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_events() -> Vec<Event> {
vec![
Event::Click { x: 10, y: 20 },
Event::KeyPress('h'),
Event::KeyPress('i'),
Event::Timer("heartbeat".to_string()),
Event::NetworkData("hello".to_string()),
Event::Click { x: 5, y: 5 },
Event::NetworkData("world".to_string()),
Event::Timer("refresh".to_string()),
Event::Quit,
Event::Click { x: 0, y: 0 }, // ignored
]
}
#[test]
fn test_run_until_quit() {
let state = run_until_quit(test_events(), AppState::new());
assert_eq!(state.clicks, 2);
assert_eq!(state.keys, "hi");
assert_eq!(state.timers, 2);
assert_eq!(state.network_msgs.len(), 2);
}
#[test]
fn test_quit_stops_processing() {
let events = vec![
Event::Click { x: 0, y: 0 },
Event::Quit,
Event::Click { x: 0, y: 0 }, // should not be processed
];
let state = run_until_quit(events, AppState::new());
assert_eq!(state.clicks, 1);
}
#[test]
fn test_event_loop_queue() {
let mut el = EventLoop::new(AppState::new());
el.push_many(test_events());
el.run();
assert_eq!(el.state.clicks, 2);
assert_eq!(el.state.keys, "hi");
}
#[test]
fn test_dispatch_click() {
let s = dispatch(AppState::new(), &Event::Click { x: 5, y: 5 });
assert_eq!(s.clicks, 1);
}
#[test]
fn test_dispatch_network() {
let s = dispatch(AppState::new(), &Event::NetworkData("test".to_string()));
assert_eq!(s.network_msgs, vec!["test"]);
}
#[test]
fn test_empty_events() {
let state = run_until_quit(vec![], AppState::new());
assert_eq!(state, AppState::new());
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Event type | enum Event | type event variant |
| State update | Struct spread ..state | Record update { state with … } |
| Loop style | fold (no short-circuit) | Tail recursion with pattern match |
| Queue | VecDeque for mutable queue | List head pattern |
Quit handling | break in imperative loop | \| Quit :: _ -> state |
| Handler dispatch | Single dispatch function | Record of functions |
The functional event loop pattern is the foundation of Elm-style architecture and Redux. A pure dispatch function makes state transitions testable without side effects — each event produces a predictable new state.
OCaml Approach
OCaml uses a record handler with fields on_click, on_key, on_timer, on_network. run_event_loop ~handler ~init events is a recursive loop state events function that pattern-matches on the head event. Quit terminates by returning the current state; other events dispatch to the appropriate handler field. This approach decouples event routing from state logic.
Full Source
#![allow(clippy::all)]
// 1001: Simple Event Loop
// Poll events, dispatch enum handlers, accumulate state
use std::collections::VecDeque;
// --- Event enum ---
#[derive(Debug, Clone, PartialEq)]
enum Event {
Click { x: i32, y: i32 },
KeyPress(char),
Timer(String),
NetworkData(String),
Quit,
}
// --- Application state ---
#[derive(Debug, Clone, PartialEq)]
struct AppState {
clicks: u32,
keys: String,
timers: u32,
network_msgs: Vec<String>,
}
impl AppState {
fn new() -> Self {
AppState {
clicks: 0,
keys: String::new(),
timers: 0,
network_msgs: Vec::new(),
}
}
}
// --- Pure functional dispatch: one event → next state ---
fn dispatch(state: AppState, event: &Event) -> AppState {
match event {
Event::Click { .. } => AppState {
clicks: state.clicks + 1,
..state
},
Event::KeyPress(c) => AppState {
keys: format!("{}{}", state.keys, c),
..state
},
Event::Timer(_) => AppState {
timers: state.timers + 1,
..state
},
Event::NetworkData(msg) => {
let mut msgs = state.network_msgs.clone();
msgs.push(msg.clone());
AppState {
network_msgs: msgs,
..state
}
}
Event::Quit => state, // handled by loop
}
}
// --- Approach 1: Functional event loop over a Vec ---
fn run_event_loop(events: Vec<Event>, init: AppState) -> AppState {
events.iter().fold(init, |state, event| {
if event == &Event::Quit {
state
}
// stop processing new events via fold
else {
dispatch(state, event)
}
})
}
// Better version that actually stops at Quit:
fn run_until_quit(events: Vec<Event>, mut state: AppState) -> AppState {
for event in events {
match event {
Event::Quit => break,
e => state = dispatch(state, &e),
}
}
state
}
// --- Approach 2: Event loop with a queue (mutable, real-world style) ---
struct EventLoop {
queue: VecDeque<Event>,
state: AppState,
}
impl EventLoop {
fn new(state: AppState) -> Self {
EventLoop {
queue: VecDeque::new(),
state,
}
}
fn push(&mut self, event: Event) {
self.queue.push_back(event);
}
fn push_many(&mut self, events: Vec<Event>) {
for e in events {
self.queue.push_back(e);
}
}
fn run(&mut self) {
while let Some(event) = self.queue.pop_front() {
match event {
Event::Quit => break,
e => self.state = dispatch(self.state.clone(), &e),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_events() -> Vec<Event> {
vec![
Event::Click { x: 10, y: 20 },
Event::KeyPress('h'),
Event::KeyPress('i'),
Event::Timer("heartbeat".to_string()),
Event::NetworkData("hello".to_string()),
Event::Click { x: 5, y: 5 },
Event::NetworkData("world".to_string()),
Event::Timer("refresh".to_string()),
Event::Quit,
Event::Click { x: 0, y: 0 }, // ignored
]
}
#[test]
fn test_run_until_quit() {
let state = run_until_quit(test_events(), AppState::new());
assert_eq!(state.clicks, 2);
assert_eq!(state.keys, "hi");
assert_eq!(state.timers, 2);
assert_eq!(state.network_msgs.len(), 2);
}
#[test]
fn test_quit_stops_processing() {
let events = vec![
Event::Click { x: 0, y: 0 },
Event::Quit,
Event::Click { x: 0, y: 0 }, // should not be processed
];
let state = run_until_quit(events, AppState::new());
assert_eq!(state.clicks, 1);
}
#[test]
fn test_event_loop_queue() {
let mut el = EventLoop::new(AppState::new());
el.push_many(test_events());
el.run();
assert_eq!(el.state.clicks, 2);
assert_eq!(el.state.keys, "hi");
}
#[test]
fn test_dispatch_click() {
let s = dispatch(AppState::new(), &Event::Click { x: 5, y: 5 });
assert_eq!(s.clicks, 1);
}
#[test]
fn test_dispatch_network() {
let s = dispatch(AppState::new(), &Event::NetworkData("test".to_string()));
assert_eq!(s.network_msgs, vec!["test"]);
}
#[test]
fn test_empty_events() {
let state = run_until_quit(vec![], AppState::new());
assert_eq!(state, AppState::new());
}
}#[cfg(test)]
mod tests {
use super::*;
fn test_events() -> Vec<Event> {
vec![
Event::Click { x: 10, y: 20 },
Event::KeyPress('h'),
Event::KeyPress('i'),
Event::Timer("heartbeat".to_string()),
Event::NetworkData("hello".to_string()),
Event::Click { x: 5, y: 5 },
Event::NetworkData("world".to_string()),
Event::Timer("refresh".to_string()),
Event::Quit,
Event::Click { x: 0, y: 0 }, // ignored
]
}
#[test]
fn test_run_until_quit() {
let state = run_until_quit(test_events(), AppState::new());
assert_eq!(state.clicks, 2);
assert_eq!(state.keys, "hi");
assert_eq!(state.timers, 2);
assert_eq!(state.network_msgs.len(), 2);
}
#[test]
fn test_quit_stops_processing() {
let events = vec![
Event::Click { x: 0, y: 0 },
Event::Quit,
Event::Click { x: 0, y: 0 }, // should not be processed
];
let state = run_until_quit(events, AppState::new());
assert_eq!(state.clicks, 1);
}
#[test]
fn test_event_loop_queue() {
let mut el = EventLoop::new(AppState::new());
el.push_many(test_events());
el.run();
assert_eq!(el.state.clicks, 2);
assert_eq!(el.state.keys, "hi");
}
#[test]
fn test_dispatch_click() {
let s = dispatch(AppState::new(), &Event::Click { x: 5, y: 5 });
assert_eq!(s.clicks, 1);
}
#[test]
fn test_dispatch_network() {
let s = dispatch(AppState::new(), &Event::NetworkData("test".to_string()));
assert_eq!(s.network_msgs, vec!["test"]);
}
#[test]
fn test_empty_events() {
let state = run_until_quit(vec![], AppState::new());
assert_eq!(state, AppState::new());
}
}
Deep Comparison
Simple Event Loop — Comparison
Core Insight
An event loop is an infinite fold: state = fold dispatch initial_state event_stream. Making dispatch a pure function (State, Event) -> State gives testability, reproducibility, and the foundation for time-travel debugging (like Redux).
OCaml Approach
type event = Click | KeyPress | ...{ on_click; on_key; on_timer; on_network }run_event_loop is recursive loop state events — structural recursionQuit to stop{ s with clicks = ... }Rust Approach
enum Event { Click { x, y }, KeyPress(char), ... } — same ADT patterndispatch(state: AppState, event: &Event) -> AppState — pure functionrun_until_quit uses a for loop with break on QuitEventLoop struct wraps VecDeque<Event> for real-world queue usageAppState { clicks: state.clicks + 1, ..state } struct update syntax mirrors OCamlComparison Table
| Concept | OCaml | Rust |
|---|---|---|
| Event type | type event = Click \| KeyPress \| ... | enum Event { Click { x, y }, ... } |
| State update | { s with clicks = s.clicks + 1 } | AppState { clicks: s.clicks + 1, ..s } |
| Dispatch function | handler.on_click x y state | dispatch(state, &event) match |
| Loop idiom | Tail-recursive loop state events | for event in events { match event } |
| Stop at Quit | Quit :: _ -> state (base case) | Event::Quit => break |
| Queue-based | Queue.pop + while | VecDeque::pop_front() + while let |
| Testability | Pure run_event_loop function | Pure dispatch + pure run_until_quit |
std vs tokio
| Aspect | std version | tokio version |
|---|---|---|
| Runtime | OS threads via std::thread | Async tasks on tokio runtime |
| Synchronization | std::sync::Mutex, Condvar | tokio::sync::Mutex, channels |
| Channels | std::sync::mpsc (unbounded) | tokio::sync::mpsc (bounded, async) |
| Blocking | Thread blocks on lock/recv | Task yields, runtime switches tasks |
| Overhead | One OS thread per task | Many tasks per thread (M:N) |
| Best for | CPU-bound, simple concurrency | I/O-bound, high-concurrency servers |
Exercises
Resize(u32, u32) event and handle it in dispatch by adding width/height fields to AppState.run_until_quit(queue: &mut VecDeque<Event>, init: AppState) -> AppState using the imperative while let loop.String description of each event to a Vec<String> log.Vec<AppState> history and add an Undo event that pops the last state.Map ordered by event priority, processing higher-priority events first.