๐Ÿฆ€ Functional Rust

1001: Simple Event Loop

Difficulty: Intermediate Category: Async / Concurrency FP Patterns Concept: Dispatch typed events to handlers, accumulate state, stop at Quit Key Insight: The functional event loop is just a `fold`: `(State, Event) -> State`; the mutable queue version mirrors real frameworks but the core `dispatch` function remains pure and testable

Versions

DirectoryDescription
`std/`Standard library version using `std::sync`, `std::thread`
`tokio/`Tokio async runtime version using `tokio::sync`, `tokio::spawn`

Running

# Standard library version
cd std && cargo test

# Tokio version
cd tokio && cargo test
// 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),
            }
        }
    }
}

fn main() {
    let events = 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 after Quit
    ];

    let final_state = run_until_quit(events, AppState::new());
    println!("clicks={} keys={} timers={} msgs={}",
        final_state.clicks, final_state.keys,
        final_state.timers, final_state.network_msgs.len());
}

#[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());
    }
}
(* 1001: Simple Event Loop *)
(* Poll events, dispatch enum handlers, process until Quit *)

(* --- Event types --- *)

type event =
  | Click of int * int         (* x, y *)
  | KeyPress of char
  | Timer of string            (* timer name *)
  | NetworkData of string      (* payload *)
  | Quit

type 'state handler = {
  on_click: int -> int -> 'state -> 'state;
  on_key: char -> 'state -> 'state;
  on_timer: string -> 'state -> 'state;
  on_network: string -> 'state -> 'state;
}

(* --- Event loop: dispatch events to handlers, accumulate state --- *)

let run_event_loop ~handler ~init events =
  let rec loop state = function
    | [] -> state
    | Quit :: _ -> state
    | Click (x, y) :: rest -> loop (handler.on_click x y state) rest
    | KeyPress c :: rest -> loop (handler.on_key c state) rest
    | Timer name :: rest -> loop (handler.on_timer name state) rest
    | NetworkData s :: rest -> loop (handler.on_network s state) rest
  in
  loop init events

(* --- Approach 1: Pure functional event loop --- *)

type app_state = {
  clicks: int;
  keys: string;
  timers: int;
  network_msgs: string list;
}

let initial_state = { clicks = 0; keys = ""; timers = 0; network_msgs = [] }

let app_handler = {
  on_click = (fun _x _y s -> { s with clicks = s.clicks + 1 });
  on_key = (fun c s -> { s with keys = s.keys ^ String.make 1 c });
  on_timer = (fun _name s -> { s with timers = s.timers + 1 });
  on_network = (fun msg s -> { s with network_msgs = msg :: s.network_msgs });
}

let () =
  let events = [
    Click (10, 20);
    KeyPress 'h';
    KeyPress 'i';
    Timer "heartbeat";
    NetworkData "hello";
    Click (5, 5);
    NetworkData "world";
    Timer "refresh";
    Quit;
    Click (0, 0);  (* ignored after Quit *)
  ] in
  let final_state = run_event_loop ~handler:app_handler ~init:initial_state events in
  assert (final_state.clicks = 2);
  assert (final_state.keys = "hi");
  assert (final_state.timers = 2);
  assert (List.length final_state.network_msgs = 2);
  Printf.printf "Approach 1: clicks=%d keys=%s timers=%d msgs=%d\n"
    final_state.clicks final_state.keys final_state.timers
    (List.length final_state.network_msgs)

(* --- Approach 2: Stateful event loop with mutation --- *)

let () =
  let q = Queue.create () in
  List.iter (Queue.push q) [Click(1,1); KeyPress 'x'; Timer "t1"; Quit];
  (* Queue is FIFO โ€” push order is preserved when popping *)
  let event_count = ref 0 in
  let rec loop () =
    if Queue.is_empty q then ()
    else match Queue.pop q with
      | Quit -> ()
      | _ -> incr event_count; loop ()
  in
  loop ();
  assert (!event_count = 3);
  Printf.printf "Approach 2 (stateful loop): %d events processed\n" !event_count

let () = Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed 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 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 idiomTail-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`
TestabilityPure `run_event_loop` functionPure `dispatch` + pure `run_until_quit`

std vs tokio

Aspectstd versiontokio version
RuntimeOS 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)
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