ExamplesBy LevelBy TopicLearning Paths
524 Intermediate GitHub

Builder Pattern with Closures

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Builder Pattern with Closures" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. The builder pattern addresses the "telescoping constructor" problem: when a type has many optional fields, constructors become unwieldy and error-prone. Key difference from OCaml: 1. **Struct generics**: Rust builders often avoid generics on the struct by boxing callbacks (`Box<dyn Fn>`); OCaml records store functions directly without boxing annotation.

Tutorial

The Problem

The builder pattern addresses the "telescoping constructor" problem: when a type has many optional fields, constructors become unwieldy and error-prone. Rust's builder idiom (fluent API) is widespread in production code — reqwest::ClientBuilder, tokio::runtime::Builder, std::thread::Builder all use it. Adding closures to builders enables behavior injection: instead of just configuring data fields, callers can inject callbacks for connection events, error handlers, or transformation pipelines. This makes APIs both configurable and extensible without requiring trait implementations.

🎯 Learning Outcomes

  • • How to combine the builder pattern with closure callbacks for behavior injection
  • • Why Box<dyn Fn(&str)> stores callbacks in builder structs without generics on the struct itself
  • • How method chaining (self -> Self) works with closure-accepting methods
  • • How Default provides sensible no-op closures for optional callbacks
  • • Where this pattern appears: HTTP client builders, async runtime builders, test harnesses
  • Code Example

    pub struct ServerBuilder {
        config: ServerConfig,
    }
    
    impl ServerBuilder {
        pub fn on_connect(mut self, f: impl Fn(&str) + 'static) -> Self {
            self.config.on_connect = Box::new(f);
            self
        }
    
        pub fn build(self) -> ServerConfig { self.config }
    }

    Key Differences

  • Struct generics: Rust builders often avoid generics on the struct by boxing callbacks (Box<dyn Fn>); OCaml records store functions directly without boxing annotation.
  • Fluent chaining: Rust self -> Self enables Builder::new().host("x").port(80).build() in idiomatic Rust; OCaml achieves this with function composition or |> pipelines.
  • No-op defaults: Rust's Default trait provides a no-op closure; OCaml uses option to represent absence, calling Option.iter on_connect addr at use time.
  • Ownership model: Rust builders consume self on each step (mut self pattern) preventing reuse after building; OCaml records are immutable by default — functional update creates a new record each step.
  • OCaml Approach

    OCaml builders are typically records with optional fields using option types for callbacks. A builder function takes a record and returns an updated copy using functional update syntax. Callbacks are plain functions stored in option fields:

    type server_config = {
      host: string; port: int;
      on_connect: (string -> unit) option;
    }
    let with_on_connect f cfg = { cfg with on_connect = Some f }
    

    Full Source

    #![allow(clippy::all)]
    //! Builder Pattern with Closures
    //!
    //! Closure-based configuration in builder APIs.
    
    /// Server configuration with closure callback.
    pub struct ServerConfig {
        pub host: String,
        pub port: u16,
        pub max_connections: usize,
        pub timeout_ms: u64,
        on_connect: Box<dyn Fn(&str)>,
    }
    
    impl std::fmt::Debug for ServerConfig {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            f.debug_struct("ServerConfig")
                .field("host", &self.host)
                .field("port", &self.port)
                .field("max_connections", &self.max_connections)
                .field("timeout_ms", &self.timeout_ms)
                .field("on_connect", &"<fn>")
                .finish()
        }
    }
    
    impl Default for ServerConfig {
        fn default() -> Self {
            ServerConfig {
                host: "localhost".to_string(),
                port: 8080,
                max_connections: 100,
                timeout_ms: 5000,
                on_connect: Box::new(|_| {}),
            }
        }
    }
    
    /// Builder for ServerConfig.
    pub struct ServerBuilder {
        config: ServerConfig,
    }
    
    impl ServerBuilder {
        pub fn new() -> Self {
            ServerBuilder {
                config: ServerConfig::default(),
            }
        }
    
        pub fn host(mut self, host: &str) -> Self {
            self.config.host = host.to_string();
            self
        }
    
        pub fn port(mut self, port: u16) -> Self {
            self.config.port = port;
            self
        }
    
        pub fn max_connections(mut self, max: usize) -> Self {
            self.config.max_connections = max;
            self
        }
    
        pub fn timeout_ms(mut self, ms: u64) -> Self {
            self.config.timeout_ms = ms;
            self
        }
    
        pub fn on_connect(mut self, f: impl Fn(&str) + 'static) -> Self {
            self.config.on_connect = Box::new(f);
            self
        }
    
        pub fn build(self) -> ServerConfig {
            self.config
        }
    }
    
    impl Default for ServerBuilder {
        fn default() -> Self {
            Self::new()
        }
    }
    
    impl ServerConfig {
        pub fn connect(&self, client: &str) {
            (self.on_connect)(client);
        }
    }
    
    /// Request handler builder.
    pub struct RequestHandler {
        validators: Vec<Box<dyn Fn(&str) -> Result<(), String>>>,
        transformer: Box<dyn Fn(String) -> String>,
    }
    
    impl RequestHandler {
        pub fn new() -> Self {
            RequestHandler {
                validators: Vec::new(),
                transformer: Box::new(|s| s),
            }
        }
    
        pub fn validate(mut self, f: impl Fn(&str) -> Result<(), String> + 'static) -> Self {
            self.validators.push(Box::new(f));
            self
        }
    
        pub fn transform(mut self, f: impl Fn(String) -> String + 'static) -> Self {
            self.transformer = Box::new(f);
            self
        }
    
        pub fn process(&self, input: &str) -> Result<String, String> {
            for validator in &self.validators {
                validator(input)?;
            }
            Ok((self.transformer)(input.to_string()))
        }
    }
    
    impl Default for RequestHandler {
        fn default() -> Self {
            Self::new()
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::cell::RefCell;
        use std::rc::Rc;
    
        #[test]
        fn test_server_builder_defaults() {
            let config = ServerBuilder::new().build();
            assert_eq!(config.host, "localhost");
            assert_eq!(config.port, 8080);
        }
    
        #[test]
        fn test_server_builder_custom() {
            let config = ServerBuilder::new()
                .host("0.0.0.0")
                .port(3000)
                .max_connections(500)
                .build();
    
            assert_eq!(config.host, "0.0.0.0");
            assert_eq!(config.port, 3000);
            assert_eq!(config.max_connections, 500);
        }
    
        #[test]
        fn test_server_on_connect() {
            let log = Rc::new(RefCell::new(Vec::new()));
            let log_clone = log.clone();
    
            let config = ServerBuilder::new()
                .on_connect(move |client| {
                    log_clone.borrow_mut().push(client.to_string());
                })
                .build();
    
            config.connect("client1");
            config.connect("client2");
    
            assert_eq!(*log.borrow(), vec!["client1", "client2"]);
        }
    
        #[test]
        fn test_request_handler_valid() {
            let handler = RequestHandler::new()
                .validate(|s| {
                    if s.is_empty() {
                        Err("empty input".into())
                    } else {
                        Ok(())
                    }
                })
                .transform(|s| s.to_uppercase());
    
            assert_eq!(handler.process("hello").unwrap(), "HELLO");
        }
    
        #[test]
        fn test_request_handler_invalid() {
            let handler = RequestHandler::new().validate(|s| {
                if s.len() < 3 {
                    Err("too short".into())
                } else {
                    Ok(())
                }
            });
    
            assert!(handler.process("ab").is_err());
            assert!(handler.process("abc").is_ok());
        }
    
        #[test]
        fn test_request_handler_chain() {
            let handler = RequestHandler::new()
                .validate(|s| {
                    if s.contains(' ') {
                        Err("no spaces".into())
                    } else {
                        Ok(())
                    }
                })
                .validate(|s| {
                    if s.is_empty() {
                        Err("empty".into())
                    } else {
                        Ok(())
                    }
                })
                .transform(|s| format!("[{}]", s));
    
            assert_eq!(handler.process("test").unwrap(), "[test]");
            assert!(handler.process("has space").is_err());
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::cell::RefCell;
        use std::rc::Rc;
    
        #[test]
        fn test_server_builder_defaults() {
            let config = ServerBuilder::new().build();
            assert_eq!(config.host, "localhost");
            assert_eq!(config.port, 8080);
        }
    
        #[test]
        fn test_server_builder_custom() {
            let config = ServerBuilder::new()
                .host("0.0.0.0")
                .port(3000)
                .max_connections(500)
                .build();
    
            assert_eq!(config.host, "0.0.0.0");
            assert_eq!(config.port, 3000);
            assert_eq!(config.max_connections, 500);
        }
    
        #[test]
        fn test_server_on_connect() {
            let log = Rc::new(RefCell::new(Vec::new()));
            let log_clone = log.clone();
    
            let config = ServerBuilder::new()
                .on_connect(move |client| {
                    log_clone.borrow_mut().push(client.to_string());
                })
                .build();
    
            config.connect("client1");
            config.connect("client2");
    
            assert_eq!(*log.borrow(), vec!["client1", "client2"]);
        }
    
        #[test]
        fn test_request_handler_valid() {
            let handler = RequestHandler::new()
                .validate(|s| {
                    if s.is_empty() {
                        Err("empty input".into())
                    } else {
                        Ok(())
                    }
                })
                .transform(|s| s.to_uppercase());
    
            assert_eq!(handler.process("hello").unwrap(), "HELLO");
        }
    
        #[test]
        fn test_request_handler_invalid() {
            let handler = RequestHandler::new().validate(|s| {
                if s.len() < 3 {
                    Err("too short".into())
                } else {
                    Ok(())
                }
            });
    
            assert!(handler.process("ab").is_err());
            assert!(handler.process("abc").is_ok());
        }
    
        #[test]
        fn test_request_handler_chain() {
            let handler = RequestHandler::new()
                .validate(|s| {
                    if s.contains(' ') {
                        Err("no spaces".into())
                    } else {
                        Ok(())
                    }
                })
                .validate(|s| {
                    if s.is_empty() {
                        Err("empty".into())
                    } else {
                        Ok(())
                    }
                })
                .transform(|s| format!("[{}]", s));
    
            assert_eq!(handler.process("test").unwrap(), "[test]");
            assert!(handler.process("has space").is_err());
        }
    }

    Deep Comparison

    OCaml vs Rust: Builder with Closures

    OCaml

    type server_config = {
      host: string;
      port: int;
      on_connect: string -> unit;
    }
    
    let default_config = {
      host = "localhost";
      port = 8080;
      on_connect = fun _ -> ();
    }
    
    let with_host host cfg = { cfg with host }
    let with_on_connect f cfg = { cfg with on_connect = f }
    

    Rust

    pub struct ServerBuilder {
        config: ServerConfig,
    }
    
    impl ServerBuilder {
        pub fn on_connect(mut self, f: impl Fn(&str) + 'static) -> Self {
            self.config.on_connect = Box::new(f);
            self
        }
    
        pub fn build(self) -> ServerConfig { self.config }
    }
    

    Key Differences

  • OCaml: Record update syntax { cfg with field = value }
  • Rust: Fluent builder with method chaining
  • Both: Closures as configuration callbacks
  • Rust: Box<dyn Fn> for stored callbacks
  • Both support configurable behavior via closures
  • Exercises

  • Retry builder: Add an on_retry(f: impl Fn(u32, &str) + 'static) callback to ServerBuilder that receives the attempt number and error message, and a max_retries(n: u32) field.
  • Transform pipeline: Add add_transform(f: impl Fn(String) -> String + 'static) to the builder that accumulates multiple transforms, applied in registration order when a request arrives.
  • Validation in build: Make ServerBuilder::build() return Result<ServerConfig, String> that validates the port is in range 1..=65535 and the host is non-empty before returning the config.
  • Open Source Repos