๐Ÿฆ€ Functional Rust

479: replace(), replacen()

Difficulty: 1 Level: Beginner Replace substrings and filter characters โ€” with the same flexible pattern system.

The Problem This Solves

Python's `str.replace(old, new, count)` and JavaScript's `str.replace()`/`str.replaceAll()` are fundamental. Rust's `.replace()` and `.replacen()` cover the same ground. What makes Rust distinctive here is consistency: the same pattern system from `.find()`, `.split()`, and `.contains()` works for `.replace()` too. You can replace a `char`, a `&str`, or use a closure as the pattern. `s.replace(|c: char| c.is_ascii_digit(), "")` removes all digits โ€” without a regex crate. This composability is Rust's design philosophy: one pattern trait, used everywhere. There's also `.retain()` โ€” an in-place character filter that modifies the `String` directly. Python's equivalent would be `"".join(c for c in s if keep(c))`, which allocates a new string. `retain()` modifies in place, keeping the same allocation.

The Intuition

`.replace(pat, replacement)` scans for all matches of `pat` and returns a new `String` with each occurrence replaced. The original is unchanged (immutable borrow). `.replacen(pat, replacement, n)` limits to the first `n` occurrences โ€” like Python's `s.replace(old, new, count)`. `.retain(|c: char| ...)` is different: it modifies the string in place, keeping only characters where the closure returns `true`. No new allocation. Think of it as an in-place filter for characters. Key mental model: `replace` = new string, never touches the original. `retain` = mutates in place.

How It Works in Rust

let s = "Hello, World! Hello, Rust!";

// replace โ€” all occurrences, returns new String
s.replace("Hello", "Hi");    // "Hi, World! Hi, Rust!"
s.replace('!', ".");          // char pattern โ€” replaces all '!'

// replacen โ€” first n occurrences only
s.replacen("Hello", "Hi", 1); // "Hi, World! Hello, Rust!" โ† only first

// Original is unchanged
let orig = String::from("hello world");
let modified = orig.replace("world", "rust");
println!("{}", orig);     // "hello world" โ€” untouched
println!("{}", modified); // "hello rust"

// retain โ€” in-place character filter (no new allocation)
let mut s2 = String::from("h3ll0 w0rld");
s2.retain(|c| !c.is_ascii_digit());
// s2 = "hll wrld"

// Pattern as closure โ€” remove all non-alphanumeric
let mut slug = String::from("Hello, World! 2024");
slug.retain(|c| c.is_alphanumeric() || c == '-' || c == ' ');
// "Hello World 2024"

// Chain replacements (each returns a new String)
"foo bar foo"
 .replace("foo", "qux")
 .replace("bar", "quux")
// "qux quux qux"

What This Unlocks

Key Differences

ConceptOCamlRust
Replace allCustom `replace_all` (Buffer loop)`s.replace(pat, rep)`
Replace first NCustom loop`s.replacen(pat, rep, n)`
Replace char`String.map``s.replace('c', "rep")`
In-place filter`Bytes.map` (not truly in-place)`s.retain(\c\...)`
Pattern typesChar or manual`char`, `&str`, `&[char]`, closure
ReturnsNew stringNew `String` (original unchanged)
// 479. replace(), replacen()
fn main() {
    let s = "Hello, World! Hello, Rust!";
    println!("{}", s.replace("Hello", "Hi"));       // all
    println!("{}", s.replacen("Hello", "Hi", 1));   // first only
    println!("{}", s.replace('!', "."));             // char pattern
    println!("{}", "a,,b,,c".replace(",,", ","));   // multi-char

    // replace returns new String โ€” original unchanged
    let orig = String::from("hello world");
    let modified = orig.replace("world", "rust");
    println!("orig: {}, modified: {}", orig, modified);

    // retain โ€” in-place filter
    let mut s2 = String::from("h3ll0 w0rld");
    s2.retain(|c| !c.is_ascii_digit());
    println!("retain: {}", s2);

    // Chain replacements
    println!("{}", "foo bar foo".replace("foo","qux").replace("bar","quux"));
}

#[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"); }
}
(* 479. String replacing โ€“ OCaml *)
let replace_all s pat rep =
  let lp=String.length pat and ls=String.length s in
  let buf=Buffer.create ls in
  let i=ref 0 in
  while !i <= ls-lp do
    if String.sub s !i lp = pat
    then (Buffer.add_string buf rep; i:= !i+lp)
    else (Buffer.add_char buf s.[!i]; incr i)
  done;
  while !i < ls do Buffer.add_char buf s.[!i]; incr i done;
  Buffer.contents buf

let () =
  let s = "Hello, World! Hello, OCaml!" in
  Printf.printf "%s\n" (replace_all s "Hello" "Hi");
  Printf.printf "%s\n" (String.map (fun c -> if c=' ' then '_' else c) s)