ExamplesBy LevelBy TopicLearning Paths
591 Advanced

Functional Builder Pattern

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Functional Builder Pattern" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. The builder pattern addresses the "telescoping constructor" problem — many optional fields make constructors unwieldy. Key difference from OCaml: 1. **Consuming vs functional update**: Rust consumes `self` at each step (ownership transfer); OCaml creates a new record at each step (GC

Tutorial

The Problem

The builder pattern addresses the "telescoping constructor" problem — many optional fields make constructors unwieldy. The functional variant uses consuming methods (self -> Self) instead of &mut self, creating an immutable chain. Each method returns a new value with one field changed. This style is prevalent in Rust's standard library (std::thread::Builder) and ecosystem (reqwest::ClientBuilder). It is also related to OCaml's record functional update syntax { record with field = new_val }.

🎯 Learning Outcomes

  • • How consuming builder methods fn field(mut self, val: T) -> Self enable method chaining
  • • How Default provides sensible starting configuration for builders
  • • How functional update eliminates the need for separate Builder and Config types
  • • How to implement build() that validates the configuration before returning it
  • • Where the functional builder pattern appears: HTTP clients, runtime builders, parser configs
  • Code Example

    impl Config {
        fn host(mut self, h: impl Into<String>) -> Self {
            self.host = h.into();
            self
        }
        fn port(mut self, p: u16) -> Self {
            self.port = p;
            self
        }
        fn tls(mut self, b: bool) -> Self {
            self.tls = b;
            self
        }
    }
    
    let cfg = Config::default()
        .host("api.example.com")
        .port(443)
        .tls(true)
        .build()?;

    Key Differences

  • Consuming vs functional update: Rust consumes self at each step (ownership transfer); OCaml creates a new record at each step (GC-managed copy).
  • Method syntax: Rust .host("x") is a method call; OCaml with_host "x" config is a function call, typically chained with |>.
  • Validation: Rust build() can return Result<Config, Error> for validation; OCaml typically validates in a separate check function.
  • Type safety: Both enforce that missing fields have defaults through the Default trait or default record value.
  • OCaml Approach

    OCaml uses functional record update syntax directly:

    type config = { host: string; port: int; tls: bool; timeout: float }
    let default_config = { host = "localhost"; port = 80; tls = false; timeout = 30.0 }
    let with_host h c = { c with host = h }
    let with_port p c = { c with port = p }
    (* Usage: default_config |> with_host "example.com" |> with_port 443 *)
    

    Full Source

    #![allow(clippy::all)]
    //! # Functional Builder Pattern
    //!
    //! Builder pattern using consuming methods that return Self for chaining.
    
    /// Network connection configuration.
    #[derive(Debug, Clone, PartialEq)]
    pub struct Config {
        pub host: String,
        pub port: u16,
        pub timeout: f64,
        pub retries: u32,
        pub tls: bool,
    }
    
    impl Default for Config {
        fn default() -> Self {
            Config {
                host: "localhost".into(),
                port: 80,
                timeout: 30.0,
                retries: 3,
                tls: false,
            }
        }
    }
    
    impl Config {
        /// Create a new config with defaults.
        pub fn new() -> Self {
            Self::default()
        }
    
        /// Set the host (consuming builder method).
        pub fn host(mut self, h: impl Into<String>) -> Self {
            self.host = h.into();
            self
        }
    
        /// Set the port.
        pub fn port(mut self, p: u16) -> Self {
            self.port = p;
            self
        }
    
        /// Set the timeout in seconds.
        pub fn timeout(mut self, t: f64) -> Self {
            self.timeout = t;
            self
        }
    
        /// Set the retry count.
        pub fn retries(mut self, r: u32) -> Self {
            self.retries = r;
            self
        }
    
        /// Enable or disable TLS.
        pub fn tls(mut self, b: bool) -> Self {
            self.tls = b;
            self
        }
    
        /// Validate and build the final config.
        pub fn build(self) -> Result<Self, String> {
            if self.host.is_empty() {
                return Err("host required".into());
            }
            if self.port == 0 {
                return Err("port required".into());
            }
            if self.timeout <= 0.0 {
                return Err("timeout must be positive".into());
            }
            Ok(self)
        }
    }
    
    /// HTTP request builder.
    #[derive(Debug, Clone, PartialEq)]
    pub struct Request {
        pub method: String,
        pub url: String,
        pub headers: Vec<(String, String)>,
        pub body: Option<String>,
    }
    
    impl Request {
        pub fn get(url: impl Into<String>) -> Self {
            Self {
                method: "GET".into(),
                url: url.into(),
                headers: Vec::new(),
                body: None,
            }
        }
    
        pub fn post(url: impl Into<String>) -> Self {
            Self {
                method: "POST".into(),
                url: url.into(),
                headers: Vec::new(),
                body: None,
            }
        }
    
        pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
            self.headers.push((key.into(), value.into()));
            self
        }
    
        pub fn body(mut self, body: impl Into<String>) -> Self {
            self.body = Some(body.into());
            self
        }
    
        pub fn json_body(self, body: impl Into<String>) -> Self {
            self.header("Content-Type", "application/json").body(body)
        }
    }
    
    /// Query builder for SQL-like queries.
    #[derive(Debug, Clone, Default)]
    pub struct Query {
        pub table: String,
        pub columns: Vec<String>,
        pub where_clause: Option<String>,
        pub order_by: Option<String>,
        pub limit: Option<usize>,
    }
    
    impl Query {
        pub fn from(table: impl Into<String>) -> Self {
            Self {
                table: table.into(),
                ..Default::default()
            }
        }
    
        pub fn select(mut self, cols: &[&str]) -> Self {
            self.columns = cols.iter().map(|s| s.to_string()).collect();
            self
        }
    
        pub fn where_eq(mut self, clause: impl Into<String>) -> Self {
            self.where_clause = Some(clause.into());
            self
        }
    
        pub fn order_by(mut self, col: impl Into<String>) -> Self {
            self.order_by = Some(col.into());
            self
        }
    
        pub fn limit(mut self, n: usize) -> Self {
            self.limit = Some(n);
            self
        }
    
        pub fn to_sql(&self) -> String {
            let cols = if self.columns.is_empty() {
                "*".to_string()
            } else {
                self.columns.join(", ")
            };
    
            let mut sql = format!("SELECT {} FROM {}", cols, self.table);
    
            if let Some(ref w) = self.where_clause {
                sql.push_str(&format!(" WHERE {}", w));
            }
            if let Some(ref o) = self.order_by {
                sql.push_str(&format!(" ORDER BY {}", o));
            }
            if let Some(l) = self.limit {
                sql.push_str(&format!(" LIMIT {}", l));
            }
    
            sql
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_config_builder() {
            let cfg = Config::default()
                .host("api.example.com")
                .port(443)
                .tls(true)
                .timeout(60.0)
                .build()
                .unwrap();
    
            assert_eq!(cfg.host, "api.example.com");
            assert_eq!(cfg.port, 443);
            assert!(cfg.tls);
        }
    
        #[test]
        fn test_config_validation_empty_host() {
            let result = Config::default().host("").build();
            assert!(result.is_err());
        }
    
        #[test]
        fn test_config_validation_zero_port() {
            let result = Config::default().port(0).build();
            assert!(result.is_err());
        }
    
        #[test]
        fn test_config_clone_and_modify() {
            let base = Config::default().retries(5);
            let dev = base.clone().host("dev.local");
            let prod = base.host("prod.example.com").tls(true);
    
            assert_eq!(dev.host, "dev.local");
            assert_eq!(prod.host, "prod.example.com");
            assert_eq!(dev.retries, 5);
            assert_eq!(prod.retries, 5);
        }
    
        #[test]
        fn test_request_builder() {
            let req = Request::get("https://api.example.com/users")
                .header("Authorization", "Bearer token")
                .header("Accept", "application/json");
    
            assert_eq!(req.method, "GET");
            assert_eq!(req.headers.len(), 2);
        }
    
        #[test]
        fn test_request_json_body() {
            let req = Request::post("https://api.example.com/users").json_body(r#"{"name": "Alice"}"#);
    
            assert_eq!(req.method, "POST");
            assert!(req
                .headers
                .iter()
                .any(|(k, v)| k == "Content-Type" && v == "application/json"));
            assert!(req.body.is_some());
        }
    
        #[test]
        fn test_query_builder() {
            let sql = Query::from("users")
                .select(&["id", "name", "email"])
                .where_eq("active = true")
                .order_by("name")
                .limit(10)
                .to_sql();
    
            assert_eq!(
                sql,
                "SELECT id, name, email FROM users WHERE active = true ORDER BY name LIMIT 10"
            );
        }
    
        #[test]
        fn test_query_default_columns() {
            let sql = Query::from("users").to_sql();
            assert_eq!(sql, "SELECT * FROM users");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_config_builder() {
            let cfg = Config::default()
                .host("api.example.com")
                .port(443)
                .tls(true)
                .timeout(60.0)
                .build()
                .unwrap();
    
            assert_eq!(cfg.host, "api.example.com");
            assert_eq!(cfg.port, 443);
            assert!(cfg.tls);
        }
    
        #[test]
        fn test_config_validation_empty_host() {
            let result = Config::default().host("").build();
            assert!(result.is_err());
        }
    
        #[test]
        fn test_config_validation_zero_port() {
            let result = Config::default().port(0).build();
            assert!(result.is_err());
        }
    
        #[test]
        fn test_config_clone_and_modify() {
            let base = Config::default().retries(5);
            let dev = base.clone().host("dev.local");
            let prod = base.host("prod.example.com").tls(true);
    
            assert_eq!(dev.host, "dev.local");
            assert_eq!(prod.host, "prod.example.com");
            assert_eq!(dev.retries, 5);
            assert_eq!(prod.retries, 5);
        }
    
        #[test]
        fn test_request_builder() {
            let req = Request::get("https://api.example.com/users")
                .header("Authorization", "Bearer token")
                .header("Accept", "application/json");
    
            assert_eq!(req.method, "GET");
            assert_eq!(req.headers.len(), 2);
        }
    
        #[test]
        fn test_request_json_body() {
            let req = Request::post("https://api.example.com/users").json_body(r#"{"name": "Alice"}"#);
    
            assert_eq!(req.method, "POST");
            assert!(req
                .headers
                .iter()
                .any(|(k, v)| k == "Content-Type" && v == "application/json"));
            assert!(req.body.is_some());
        }
    
        #[test]
        fn test_query_builder() {
            let sql = Query::from("users")
                .select(&["id", "name", "email"])
                .where_eq("active = true")
                .order_by("name")
                .limit(10)
                .to_sql();
    
            assert_eq!(
                sql,
                "SELECT id, name, email FROM users WHERE active = true ORDER BY name LIMIT 10"
            );
        }
    
        #[test]
        fn test_query_default_columns() {
            let sql = Query::from("users").to_sql();
            assert_eq!(sql, "SELECT * FROM users");
        }
    }

    Deep Comparison

    OCaml vs Rust: Functional Builder Pattern

    Builder via Method Chaining

    OCaml (Pipeline with Record Update)

    type config = { host: string; port: int; tls: bool; (* ... *) }
    
    let default_config = { host = "localhost"; port = 80; tls = false }
    
    let with_host h c = { c with host = h }
    let with_port p c = { c with port = p }
    let with_tls  b c = { c with tls = b }
    
    let cfg =
      default_config
      |> with_host "api.example.com"
      |> with_port 443
      |> with_tls true
    

    Rust (Consuming Builder)

    impl Config {
        fn host(mut self, h: impl Into<String>) -> Self {
            self.host = h.into();
            self
        }
        fn port(mut self, p: u16) -> Self {
            self.port = p;
            self
        }
        fn tls(mut self, b: bool) -> Self {
            self.tls = b;
            self
        }
    }
    
    let cfg = Config::default()
        .host("api.example.com")
        .port(443)
        .tls(true)
        .build()?;
    

    Key Differences

    AspectOCamlRust
    MutationImmutable update { c with }mut self consumed
    ChainingPipeline \|>Dot .method()
    OwnershipGC copiesMove semantics
    Clone for reuseAutomaticExplicit .clone()
    ValidationSeparate function.build() returns Result

    Benefits

  • Fluent API - Readable configuration
  • Type safety - Compiler catches missing fields
  • Immutability - Each step produces new value
  • Validation - Build step can verify constraints
  • Exercises

  • Validation: Add fn build(self) -> Result<Config, String> that returns Err if host is empty or port is 0.
  • With-fn style: Rewrite the builder as free functions fn with_host(h: &str, c: Config) -> Config that can be chained with .pipe(|c| with_host("x", c)).
  • Builder struct: Create a separate ConfigBuilder struct with &mut self methods and a build() -> Config that consumes the builder — compare API ergonomics with the consuming-self style.
  • Open Source Repos