String Replacing
Functional Programming
Tutorial
The Problem
Sanitising input, normalising separators, redacting sensitive data, and applying text patches all require replacing substrings. The key design decisions are: replace all occurrences vs. a fixed count; return a new string vs. modify in-place; match on exact substring vs. a predicate. Rust's replace API covers all these cases with allocating (replace/replacen) and in-place (retain) variants.
🎯 Learning Outcomes
.replace(from, to) returning a new String.replacen(from, to, n)String::retain(|c| predicate)replace always allocates a new string even if no substitution occurspat can be a char, &str, or closure in both replace and retainCode Example
#![allow(clippy::all)]
// 479. replace(), replacen()
#[cfg(test)]
mod tests {
#[test]
fn test_replace_all() {
assert_eq!("aabaa".replace('a', "x"), "xxbxx");
}
#[test]
fn test_replacen() {
assert_eq!("aabaa".replacen('a', "x", 2), "xxbaa");
}
#[test]
fn test_no_match() {
assert_eq!("hello".replace("xyz", "abc"), "hello");
}
#[test]
fn test_retain() {
let mut s = String::from("h3llo");
s.retain(|c| c.is_alphabetic());
assert_eq!(s, "hllo");
}
}Key Differences
replace/replacen are on str with no imports; OCaml requires Str (a separate library) or Re (third-party).retain**: Rust's retain modifies a String without reallocating (if possible); OCaml always allocates a new string for filtered results.replacen**: Rust has a n-replacement limit built in; OCaml's Str has replace_first (one) and global_replace (all) but no direct n-replacement.replace accepts char, &str, &[char], and closures; Str.global_replace requires a compiled Str.regexp.OCaml Approach
OCaml's standard library has no replace; the Str module provides:
Str.global_replace (Str.regexp_string "a") "x" "aabaa" (* "xxbxx" *)
Str.replace_first (Str.regexp_string "a") "x" "aabaa" (* "xabaa" *)
For replacen behaviour, manual recursion or String.concat is needed. In-place filtering:
let retain pred s =
String.concat "" (List.filter_map
(fun c -> if pred c then Some (String.make 1 c) else None)
(List.of_seq (String.to_seq s)))
Full Source
#![allow(clippy::all)]
// 479. replace(), replacen()
#[cfg(test)]
mod tests {
#[test]
fn test_replace_all() {
assert_eq!("aabaa".replace('a', "x"), "xxbxx");
}
#[test]
fn test_replacen() {
assert_eq!("aabaa".replacen('a', "x", 2), "xxbaa");
}
#[test]
fn test_no_match() {
assert_eq!("hello".replace("xyz", "abc"), "hello");
}
#[test]
fn test_retain() {
let mut s = String::from("h3llo");
s.retain(|c| c.is_alphabetic());
assert_eq!(s, "hllo");
}
}
✓ Tests
Rust test suite
#[cfg(test)]
mod tests {
#[test]
fn test_replace_all() {
assert_eq!("aabaa".replace('a', "x"), "xxbxx");
}
#[test]
fn test_replacen() {
assert_eq!("aabaa".replacen('a', "x", 2), "xxbaa");
}
#[test]
fn test_no_match() {
assert_eq!("hello".replace("xyz", "abc"), "hello");
}
#[test]
fn test_retain() {
let mut s = String::from("h3llo");
s.retain(|c| c.is_alphabetic());
assert_eq!(s, "hllo");
}
}
Exercises
render(template: &str, vars: &HashMap<&str, &str>) -> String that replaces {{key}} placeholders using replace in a loop over the map.redact_emails(text: &str) -> String using a Regex (from the regex crate) to replace email addresses with [REDACTED].criterion to compare s.retain(|c| c.is_alphanumeric()) against s.chars().filter(|c| c.is_alphanumeric()).collect::<String>() on a 10KB string.