ExamplesBy LevelBy TopicLearning Paths
495 Fundamental

String Template Pattern

Functional Programming

Tutorial

The Problem

format! requires the template to be known at compile time. For user-defined or config-driven templates — email subjects, report headers, webhook payloads — the template is a runtime string. A simple but naive implementation calls string.replace("{{key}}", value) for each variable: this scans the entire string N times (one per variable) and allocates N intermediate strings. The efficient approach is a single streaming pass: scan for {{, extract the key, look it up, emit the value, and advance past }} — O(template_length) with at most one allocation for the result.

🎯 Learning Outcomes

  • • Implement a streaming template parser using find + manual index arithmetic
  • • Emit literal text and substituted values into a pre-allocated String
  • • Handle missing variables gracefully (keep placeholder vs. empty string)
  • • Compare the streaming approach against the naive replace loop
  • • Use String::with_capacity to pre-allocate based on template size
  • Code Example

    #![allow(clippy::all)]
    // 495. Template string pattern
    use std::collections::HashMap;
    
    fn render(template: &str, vars: &HashMap<&str, &str>) -> String {
        let mut result = template.to_string();
        for (key, value) in vars {
            let placeholder = format!("{{{{{}}}}}", key);
            result = result.replace(&placeholder, value);
        }
        result
    }
    
    fn render_fn<F: Fn(&str) -> Option<String>>(template: &str, lookup: F) -> String {
        let mut out = String::with_capacity(template.len());
        let mut rest = template;
        while let Some(start) = rest.find("{{") {
            out.push_str(&rest[..start]);
            rest = &rest[start + 2..];
            if let Some(end) = rest.find("}}") {
                let key = &rest[..end];
                out.push_str(&lookup(key).unwrap_or_else(|| format!("{{{{{}}}}}", key)));
                rest = &rest[end + 2..];
            } else {
                out.push_str("{{"); // unclosed — keep as-is
            }
        }
        out.push_str(rest);
        out
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_render() {
            let mut v = HashMap::new();
            v.insert("x", "10");
            v.insert("y", "20");
            assert_eq!(render("{{x}}+{{y}}", &v), "10+20");
        }
        #[test]
        fn test_missing() {
            let v: HashMap<&str, &str> = HashMap::new();
            assert_eq!(render("{{x}}", &v), "{{x}}"); // placeholder kept
        }
    }

    Key Differences

  • Single pass: Rust's streaming approach using find + slice advancement is O(N); the naive replace loop is O(N×M) where M is the number of variables.
  • Slice advancement: Rust's rest = &rest[end + 2..] advances without copying; OCaml uses index arithmetic with String.index_from_opt.
  • **with_capacity**: Rust pre-allocates the output buffer at template.len() bytes; OCaml's Buffer.create takes a hint.
  • Closure lookup: Rust's render_fn takes a generic F: Fn(&str) -> Option<String>, enabling HashMap, function, or closure-based lookup; OCaml uses an association list or Hashtbl.
  • OCaml Approach

    let render template vars =
      let buf = Buffer.create (String.length template) in
      let rec loop i =
        match String.index_from_opt template i '{' with
        | None -> Buffer.add_substring buf template i (String.length template - i)
        | Some j when j + 1 < String.length template && template.[j+1] = '{' ->
            Buffer.add_substring buf template i (j - i);
            (match String.index_from_opt template (j+2) '}' with
             | Some k when k + 1 < String.length template && template.[k+1] = '}' ->
                 let key = String.sub template (j+2) (k - j - 2) in
                 Buffer.add_string buf (Option.value ~default:key (List.assoc_opt key vars));
                 loop (k+2)
             | _ -> Buffer.add_string buf "{{"; loop (j+2))
        | Some j -> Buffer.add_char buf template.[j]; loop (j+1)
      in
      loop 0; Buffer.contents buf
    

    Full Source

    #![allow(clippy::all)]
    // 495. Template string pattern
    use std::collections::HashMap;
    
    fn render(template: &str, vars: &HashMap<&str, &str>) -> String {
        let mut result = template.to_string();
        for (key, value) in vars {
            let placeholder = format!("{{{{{}}}}}", key);
            result = result.replace(&placeholder, value);
        }
        result
    }
    
    fn render_fn<F: Fn(&str) -> Option<String>>(template: &str, lookup: F) -> String {
        let mut out = String::with_capacity(template.len());
        let mut rest = template;
        while let Some(start) = rest.find("{{") {
            out.push_str(&rest[..start]);
            rest = &rest[start + 2..];
            if let Some(end) = rest.find("}}") {
                let key = &rest[..end];
                out.push_str(&lookup(key).unwrap_or_else(|| format!("{{{{{}}}}}", key)));
                rest = &rest[end + 2..];
            } else {
                out.push_str("{{"); // unclosed — keep as-is
            }
        }
        out.push_str(rest);
        out
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_render() {
            let mut v = HashMap::new();
            v.insert("x", "10");
            v.insert("y", "20");
            assert_eq!(render("{{x}}+{{y}}", &v), "10+20");
        }
        #[test]
        fn test_missing() {
            let v: HashMap<&str, &str> = HashMap::new();
            assert_eq!(render("{{x}}", &v), "{{x}}"); // placeholder kept
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_render() {
            let mut v = HashMap::new();
            v.insert("x", "10");
            v.insert("y", "20");
            assert_eq!(render("{{x}}+{{y}}", &v), "10+20");
        }
        #[test]
        fn test_missing() {
            let v: HashMap<&str, &str> = HashMap::new();
            assert_eq!(render("{{x}}", &v), "{{x}}"); // placeholder kept
        }
    }

    Exercises

  • Recursive templates: Extend render_fn to support nested templates — substitute values that themselves contain {{key}} placeholders, with a depth limit to prevent infinite loops.
  • Benchmark render strategies: Use criterion to compare the naive replace loop vs. render_fn for a 1KB template with 20 variables.
  • Typed templates: Design a Template<Args> type (using a struct with named fields) that validates at compile time that all required variables are provided, similar to format_args!.
  • Open Source Repos