ExamplesBy LevelBy TopicLearning Paths
1001 Intermediate

1001 — Event Loop

Functional Programming

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

  • • Model events as a Rust enum with variant data (Click { x, y }, KeyPress(char))
  • • Implement dispatch(state, event) -> AppState as a pure function
  • • Use struct update syntax AppState { clicks: state.clicks + 1, ..state } for partial updates
  • • Use fold to thread state through a sequence of events
  • • Use VecDeque for a mutable event queue with O(1) push/pop
  • • Map Rust's fold-based loop to OCaml's tail-recursive loop state events
  • Code 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

    AspectRustOCaml
    Event typeenum Eventtype event variant
    State updateStruct spread ..stateRecord update { state with … }
    Loop stylefold (no short-circuit)Tail recursion with pattern match
    QueueVecDeque for mutable queueList head pattern
    Quit handlingbreak in imperative loop\| Quit :: _ -> state
    Handler dispatchSingle dispatch functionRecord 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());
        }
    }
    ✓ Tests Rust test suite
    #[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

  • • Event type as algebraic type: type event = Click | KeyPress | ...
  • • Handler record: { on_click; on_key; on_timer; on_network }
  • run_event_loop is recursive loop state events — structural recursion
  • • Pattern match on Quit to stop
  • • State as immutable record with record update syntax { s with clicks = ... }
  • Rust Approach

  • enum Event { Click { x, y }, KeyPress(char), ... } — same ADT pattern
  • dispatch(state: AppState, event: &Event) -> AppState — pure function
  • run_until_quit uses a for loop with break on Quit
  • EventLoop struct wraps VecDeque<Event> for real-world queue usage
  • AppState { clicks: state.clicks + 1, ..state } struct update syntax mirrors OCaml
  • Comparison Table

    ConceptOCamlRust
    Event typetype event = Click \| KeyPress \| ...enum Event { Click { x, y }, ... }
    State update{ s with clicks = s.clicks + 1 }AppState { clicks: s.clicks + 1, ..s }
    Dispatch functionhandler.on_click x y statedispatch(state, &event) match
    Loop idiomTail-recursive loop state eventsfor event in events { match event }
    Stop at QuitQuit :: _ -> state (base case)Event::Quit => break
    Queue-basedQueue.pop + whileVecDeque::pop_front() + while let
    TestabilityPure run_event_loop functionPure dispatch + pure run_until_quit

    std vs tokio

    Aspectstd versiontokio version
    RuntimeOS threads via std::threadAsync tasks on tokio runtime
    Synchronizationstd::sync::Mutex, Condvartokio::sync::Mutex, channels
    Channelsstd::sync::mpsc (unbounded)tokio::sync::mpsc (bounded, async)
    BlockingThread blocks on lock/recvTask yields, runtime switches tasks
    OverheadOne OS thread per taskMany tasks per thread (M:N)
    Best forCPU-bound, simple concurrencyI/O-bound, high-concurrency servers

    Exercises

  • Add a Resize(u32, u32) event and handle it in dispatch by adding width/height fields to AppState.
  • Implement run_until_quit(queue: &mut VecDeque<Event>, init: AppState) -> AppState using the imperative while let loop.
  • Add event logging: before dispatching, push a String description of each event to a Vec<String> log.
  • Implement an undo mechanism: keep a Vec<AppState> history and add an Undo event that pops the last state.
  • In OCaml, implement a priority event queue using a Map ordered by event priority, processing higher-priority events first.
  • Open Source Repos