๐Ÿฆ€ Functional Rust

484: Cow<str> for Flexible Strings

Difficulty: 2 Level: Intermediate Avoid unnecessary heap allocations by holding either a borrowed `&str` or an owned `String` โ€” decided at runtime.

The Problem This Solves

Imagine a function that sanitizes a string: if it's already clean, return it as-is; if it needs modification, return the modified version. In Python or JavaScript, you'd return a new string either way โ€” even if nothing changed, you allocate. In Rust, returning a `String` means you always allocate a copy. Returning `&str` means you can never allocate. Neither is right. `Cow<'a, str>` ("Clone on Write") solves this. It's an enum that holds either a borrowed `&str` or an owned `String`. If your data needs no modification, you return `Cow::Borrowed(s)` โ€” zero allocation, the original `&str` passed through. If modification is needed, you return `Cow::Owned(new_string)` โ€” allocated only when necessary. This pattern appears throughout the standard library. `String::from_utf8_lossy()` returns `Cow<str>` โ€” if the input is valid UTF-8, you get a borrowed view; if replacement was needed, you get an owned `String`. Many serialization and normalization functions use the same approach.

The Intuition

`Cow<str>` is Rust's way of saying "I might need to own this, I might not โ€” decide at runtime." The mental model: Both variants implement `Deref<Target = str>`, so you can call any `str` method on a `Cow<str>` without knowing which variant it is. `cow.trim()`, `cow.contains("x")`, `cow.len()` โ€” all work transparently. There's no direct OCaml equivalent. OCaml strings are immutable, so the question of "borrow or own" doesn't arise the same way. The closest analogy in any language is Go's `[]byte` vs `string` distinction, or Scala's `lazy val`.

How It Works in Rust

use std::borrow::Cow;

// Returns borrowed if no spaces, owned if spaces found
fn ensure_no_spaces(s: &str) -> Cow<str> {
 if !s.contains(' ') {
     Cow::Borrowed(s)               // zero allocation โ€” just a view
 } else {
     Cow::Owned(s.replace(' ', "_")) // allocates only when needed
 }
}

let clean = ensure_no_spaces("hello_world");
let dirty = ensure_no_spaces("hello world");

// Both work the same โ€” Cow deref's to &str transparently
println!("{}", clean);  // "hello_world"  (no allocation)
println!("{}", dirty);  // "hello_world"  (allocated)

// Check which variant at runtime
matches!(clean, Cow::Borrowed(_))  // true
matches!(dirty, Cow::Owned(_))     // true

// Accept both &str and String in one parameter type
fn process(input: Cow<str>) -> String {
 format!("processed: {}", input.trim())  // works on both variants
}

process(Cow::Borrowed("  hello  "));
process(Cow::Owned(String::from("  world  ")));

// Cow::into_owned() โ€” get a String regardless of variant
let s: String = clean.into_owned();  // copies if Borrowed, moves if Owned

// from_utf8_lossy returns Cow<str>
let bytes = b"hello world";
let cow = String::from_utf8_lossy(bytes);  // Borrowed (valid UTF-8, no copy)

What This Unlocks

Key Differences

ConceptOCamlRust
Borrow or ownNo distinction (strings immutable)`Cow<str>` โ€” explicit borrowed vs owned
Conditional allocationAlways allocates on transform`Cow::Borrowed` avoids allocation
Uniform APIN/A`Deref<Target=str>` โ€” same methods on both
Runtime checkN/A`matches!(cow, Cow::Borrowed(_))`
Force ownedN/A`.into_owned()` โ†’ `String`
Closest analog`type cow_str = Borrowed of string \Owned of Buffer.t``Cow<'a, str>` โ€” built into std
// 484. Cow<str> for flexible strings
use std::borrow::Cow;

fn ensure_no_spaces(s: &str) -> Cow<str> {
    if !s.contains(' ') {
        Cow::Borrowed(s)          // no allocation!
    } else {
        Cow::Owned(s.replace(' ', "_"))  // allocates only when needed
    }
}

fn to_uppercase_if_needed(s: Cow<str>) -> Cow<str> {
    if s.chars().any(|c| c.is_lowercase()) {
        Cow::Owned(s.to_uppercase())
    } else {
        s  // pass through unchanged
    }
}

fn process(input: Cow<str>) -> String {
    // Can call String methods on Cow<str> via deref
    format!("processed: {}", input.trim())
}

fn main() {
    let clean = ensure_no_spaces("hello_world");
    let dirty = ensure_no_spaces("hello world");
    println!("clean borrowed: {}", matches!(clean, Cow::Borrowed(_)));
    println!("dirty owned:    {}", matches!(dirty, Cow::Owned(_)));
    println!("{}", clean);
    println!("{}", dirty);

    // Accept both &str and String
    let s1: Cow<str> = Cow::Borrowed("HELLO");
    let s2: Cow<str> = Cow::Owned(String::from("hello"));
    println!("{}", to_uppercase_if_needed(s1));
    println!("{}", to_uppercase_if_needed(s2));

    println!("{}", process(Cow::Borrowed("  hi  ")));
    println!("{}", process(Cow::Owned(String::from("  owned  "))));
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test] fn test_no_alloc()  { assert!(matches!(ensure_no_spaces("nospace"), Cow::Borrowed(_))); }
    #[test] fn test_allocs()    { assert!(matches!(ensure_no_spaces("has space"), Cow::Owned(_))); }
    #[test] fn test_content()   { assert_eq!(&*ensure_no_spaces("a b"), "a_b"); }
}
(* 484. Cow<str> concept โ€“ OCaml *)
(* OCaml has no Cow; simulate with a type *)
type cow_str = Borrowed of string | Owned of Buffer.t

let make_borrowed s = Borrowed s
let make_owned s = Owned (let b=Buffer.create (String.length s) in Buffer.add_string b s; b)

let to_string = function
  | Borrowed s -> s
  | Owned b -> Buffer.contents b

let ensure_uppercase = function
  | Borrowed s ->
    if String.for_all (fun c -> not(c>='a'&&c<='z')) s then Borrowed s
    else Owned (let b=Buffer.create (String.length s) in
                String.iter (fun c -> Buffer.add_char b (Char.uppercase_ascii c)) s; b)
  | Owned b as o -> o

let () =
  let a = make_borrowed "HELLO" in
  let b = make_borrowed "hello" in
  let ra = ensure_uppercase a in
  let rb = ensure_uppercase b in
  Printf.printf "%s %s\n" (to_string ra) (to_string rb);
  (* a was not re-allocated *)
  Printf.printf "a is borrowed: %b\n" (match ra with Borrowed _ -> true | _ -> false)