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
find + manual index arithmeticStringreplace loopString::with_capacity to pre-allocate based on template sizeCode 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
find + slice advancement is O(N); the naive replace loop is O(N×M) where M is the number of variables.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.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
render_fn to support nested templates — substitute values that themselves contain {{key}} placeholders, with a depth limit to prevent infinite loops.criterion to compare the naive replace loop vs. render_fn for a 1KB template with 20 variables.Template<Args> type (using a struct with named fields) that validates at compile time that all required variables are provided, similar to format_args!.