ExamplesBy LevelBy TopicLearning Paths
112 Advanced

112-cow-clone-on-write — Cow<T>: Clone on Write

Functional Programming

Tutorial

The Problem

String normalization, data sanitization, and text transformation often leave the majority of inputs unchanged. Cloning the entire string just to return it unmodified wastes memory and time. Cow<'a, str> (Clone-on-Write) solves this by returning a borrowed reference when no change is needed and allocating only when a modification is required.

std::borrow::Cow is used throughout the standard library — String::from_utf8_lossy, Path::to_string_lossy, and environment variable reading all use Cow to avoid unnecessary allocations.

🎯 Learning Outcomes

  • • Use Cow::Borrowed(&'a str) for zero-allocation pass-through
  • • Use Cow::Owned(String) when allocation is required
  • • Use to_mut() to lazily trigger a clone on first mutation
  • • Recognize Cow in standard library APIs
  • • Apply Cow<[T]> to slice processing
  • Code Example

    use std::borrow::Cow;
    
    fn normalize_whitespace(s: &str) -> Cow<str> {
        if s.contains('\t') {
            Cow::Owned(s.replace('\t', " "))   // allocates only here
        } else {
            Cow::Borrowed(s)                    // zero-cost borrow
        }
    }

    Key Differences

  • Explicit vs implicit: Rust's Cow makes the borrow-or-own decision visible in the type; OCaml returns the same string reference implicitly.
  • **to_mut() lazy clone**: Rust's Cow::to_mut() triggers a clone on first call and then provides &mut T; OCaml has no equivalent for mutable promotion.
  • Type annotation: -> Cow<'_, str> in the return type signals to callers whether allocation might occur; OCaml's string return type is opaque.
  • Slice variant: Cow<'_, [T]> works the same way for slices; OCaml's list/array returns handle this via GC sharing.
  • OCaml Approach

    OCaml's GC and structural sharing for immutable values provide similar behavior naturally — sharing is automatic. For string processing, OCaml would either always allocate or use explicit optional return:

    let normalize_whitespace s =
      if String.contains s '	' then
        String.map (fun c -> if c = '	' then ' ' else c) s  (* allocates *)
      else s  (* returns same string — GC sharing, no allocation *)
    

    OCaml strings are immutable since 4.06, so returning s directly shares the original without copying. The GC tracks the reference — equivalent to Cow::Borrowed but without the explicit type.

    Full Source

    #![allow(clippy::all)]
    // Example 112: Cow<T> — Clone on Write
    //
    // Cow<'a, T> = either Borrowed(&'a T) or Owned(T).
    // Allocation is deferred until — and only if — mutation is needed.
    
    use std::borrow::Cow;
    
    // Solution 1: Idiomatic Rust — conditional string modification.
    // Returns a borrowed slice when no change is needed; allocates only on mutation.
    pub fn normalize_whitespace(s: &str) -> Cow<'_, str> {
        if s.contains('\t') {
            Cow::Owned(s.replace('\t', " "))
        } else {
            Cow::Borrowed(s)
        }
    }
    
    // Solution 2: Functional — strip a prefix, borrowing when it isn't present.
    pub fn strip_prefix_cow<'a>(s: &'a str, prefix: &str) -> Cow<'a, str> {
        match s.strip_prefix(prefix) {
            Some(stripped) => Cow::Owned(stripped.to_owned()),
            None => Cow::Borrowed(s),
        }
    }
    
    // Solution 3: to_mut() demonstrates lazy cloning on a Cow<[T]>.
    // The slice is shared until the data actually needs reordering.
    pub fn ensure_sorted(v: &[i32]) -> Cow<'_, [i32]> {
        if v.windows(2).all(|w| w[0] <= w[1]) {
            Cow::Borrowed(v)
        } else {
            let mut owned = v.to_vec();
            owned.sort();
            Cow::Owned(owned)
        }
    }
    
    // Solution 4: Accumulate only when the input needs HTML escaping.
    pub fn escape_html(s: &str) -> Cow<'_, str> {
        if !s.contains(['<', '>', '&', '"']) {
            return Cow::Borrowed(s);
        }
        let mut out = String::with_capacity(s.len());
        for ch in s.chars() {
            match ch {
                '<' => out.push_str("&lt;"),
                '>' => out.push_str("&gt;"),
                '&' => out.push_str("&amp;"),
                '"' => out.push_str("&quot;"),
                c => out.push(c),
            }
        }
        Cow::Owned(out)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::borrow::Cow;
    
        // --- normalize_whitespace ---
    
        #[test]
        fn test_normalize_no_tabs_borrows() {
            let s = "hello world";
            let result = normalize_whitespace(s);
            assert_eq!(&*result, "hello world");
            assert!(matches!(result, Cow::Borrowed(_)));
        }
    
        #[test]
        fn test_normalize_with_tabs_owns() {
            let s = "hello\tworld\tthere";
            let result = normalize_whitespace(s);
            assert_eq!(&*result, "hello world there");
            assert!(matches!(result, Cow::Owned(_)));
        }
    
        #[test]
        fn test_normalize_empty_string() {
            let result = normalize_whitespace("");
            assert_eq!(&*result, "");
            assert!(matches!(result, Cow::Borrowed(_)));
        }
    
        // --- strip_prefix_cow ---
    
        #[test]
        fn test_strip_prefix_present_owns() {
            let result = strip_prefix_cow("Mr. Smith", "Mr. ");
            assert_eq!(&*result, "Smith");
            assert!(matches!(result, Cow::Owned(_)));
        }
    
        #[test]
        fn test_strip_prefix_absent_borrows() {
            let result = strip_prefix_cow("Dr. Jones", "Mr. ");
            assert_eq!(&*result, "Dr. Jones");
            assert!(matches!(result, Cow::Borrowed(_)));
        }
    
        // --- ensure_sorted ---
    
        #[test]
        fn test_sorted_input_borrows() {
            let v = [1, 2, 3, 4, 5];
            let result = ensure_sorted(&v);
            assert_eq!(&*result, &[1, 2, 3, 4, 5]);
            assert!(matches!(result, Cow::Borrowed(_)));
        }
    
        #[test]
        fn test_unsorted_input_owns() {
            let v = [3, 1, 4, 1, 5, 9, 2, 6];
            let result = ensure_sorted(&v);
            assert_eq!(&*result, &[1, 1, 2, 3, 4, 5, 6, 9]);
            assert!(matches!(result, Cow::Owned(_)));
        }
    
        #[test]
        fn test_empty_slice_borrows() {
            let v: &[i32] = &[];
            let result = ensure_sorted(v);
            assert!(matches!(result, Cow::Borrowed(_)));
        }
    
        // --- escape_html ---
    
        #[test]
        fn test_escape_html_no_special_borrows() {
            let s = "hello world";
            let result = escape_html(s);
            assert_eq!(&*result, "hello world");
            assert!(matches!(result, Cow::Borrowed(_)));
        }
    
        #[test]
        fn test_escape_html_with_special_owns() {
            let result = escape_html("<b>bold</b> & \"quoted\"");
            assert_eq!(&*result, "&lt;b&gt;bold&lt;/b&gt; &amp; &quot;quoted&quot;");
            assert!(matches!(result, Cow::Owned(_)));
        }
    
        #[test]
        fn test_escape_html_only_ampersand() {
            let result = escape_html("a & b");
            assert_eq!(&*result, "a &amp; b");
            assert!(matches!(result, Cow::Owned(_)));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::borrow::Cow;
    
        // --- normalize_whitespace ---
    
        #[test]
        fn test_normalize_no_tabs_borrows() {
            let s = "hello world";
            let result = normalize_whitespace(s);
            assert_eq!(&*result, "hello world");
            assert!(matches!(result, Cow::Borrowed(_)));
        }
    
        #[test]
        fn test_normalize_with_tabs_owns() {
            let s = "hello\tworld\tthere";
            let result = normalize_whitespace(s);
            assert_eq!(&*result, "hello world there");
            assert!(matches!(result, Cow::Owned(_)));
        }
    
        #[test]
        fn test_normalize_empty_string() {
            let result = normalize_whitespace("");
            assert_eq!(&*result, "");
            assert!(matches!(result, Cow::Borrowed(_)));
        }
    
        // --- strip_prefix_cow ---
    
        #[test]
        fn test_strip_prefix_present_owns() {
            let result = strip_prefix_cow("Mr. Smith", "Mr. ");
            assert_eq!(&*result, "Smith");
            assert!(matches!(result, Cow::Owned(_)));
        }
    
        #[test]
        fn test_strip_prefix_absent_borrows() {
            let result = strip_prefix_cow("Dr. Jones", "Mr. ");
            assert_eq!(&*result, "Dr. Jones");
            assert!(matches!(result, Cow::Borrowed(_)));
        }
    
        // --- ensure_sorted ---
    
        #[test]
        fn test_sorted_input_borrows() {
            let v = [1, 2, 3, 4, 5];
            let result = ensure_sorted(&v);
            assert_eq!(&*result, &[1, 2, 3, 4, 5]);
            assert!(matches!(result, Cow::Borrowed(_)));
        }
    
        #[test]
        fn test_unsorted_input_owns() {
            let v = [3, 1, 4, 1, 5, 9, 2, 6];
            let result = ensure_sorted(&v);
            assert_eq!(&*result, &[1, 1, 2, 3, 4, 5, 6, 9]);
            assert!(matches!(result, Cow::Owned(_)));
        }
    
        #[test]
        fn test_empty_slice_borrows() {
            let v: &[i32] = &[];
            let result = ensure_sorted(v);
            assert!(matches!(result, Cow::Borrowed(_)));
        }
    
        // --- escape_html ---
    
        #[test]
        fn test_escape_html_no_special_borrows() {
            let s = "hello world";
            let result = escape_html(s);
            assert_eq!(&*result, "hello world");
            assert!(matches!(result, Cow::Borrowed(_)));
        }
    
        #[test]
        fn test_escape_html_with_special_owns() {
            let result = escape_html("<b>bold</b> & \"quoted\"");
            assert_eq!(&*result, "&lt;b&gt;bold&lt;/b&gt; &amp; &quot;quoted&quot;");
            assert!(matches!(result, Cow::Owned(_)));
        }
    
        #[test]
        fn test_escape_html_only_ampersand() {
            let result = escape_html("a & b");
            assert_eq!(&*result, "a &amp; b");
            assert!(matches!(result, Cow::Owned(_)));
        }
    }

    Deep Comparison

    OCaml vs Rust: Cow<T> — Clone on Write

    Side-by-Side Code

    OCaml

    (* OCaml's GC handles sharing automatically — no explicit CoW needed.
       The closest idiom is a conditional copy: return the input unchanged
       when no modification is required, otherwise build a new value. *)
    
    let normalize_whitespace s =
      if String.contains s '\t' then
        String.map (fun c -> if c = '\t' then ' ' else c) s
      else
        s  (* no allocation: same string object *)
    
    let escape_html s =
      let needs_escape = String.exists (fun c ->
        c = '<' || c = '>' || c = '&' || c = '"') s in
      if needs_escape then
        (* allocate only when needed *)
        let buf = Buffer.create (String.length s) in
        String.iter (fun c ->
          Buffer.add_string buf (match c with
            | '<' -> "&lt;" | '>' -> "&gt;"
            | '&' -> "&amp;" | '"' -> "&quot;"
            | c   -> String.make 1 c)) s;
        Buffer.contents buf
      else
        s
    

    Rust (idiomatic — Cow<str>)

    use std::borrow::Cow;
    
    fn normalize_whitespace(s: &str) -> Cow<str> {
        if s.contains('\t') {
            Cow::Owned(s.replace('\t', " "))   // allocates only here
        } else {
            Cow::Borrowed(s)                    // zero-cost borrow
        }
    }
    

    Rust (functional — ensure_sorted with Cow<[T]>)

    use std::borrow::Cow;
    
    fn ensure_sorted(v: &[i32]) -> Cow<[i32]> {
        if v.windows(2).all(|w| w[0] <= w[1]) {
            Cow::Borrowed(v)           // already sorted — share the slice
        } else {
            let mut owned = v.to_vec();
            owned.sort();
            Cow::Owned(owned)          // cloned only when reordering is needed
        }
    }
    

    Type Signatures

    ConceptOCamlRust
    String typestring (immutable)&str (borrowed) / String (owned)
    "Maybe owned"no built-in — use string (GC shares)Cow<'a, str>
    Slice type'a array&[T] / Vec<T>
    "Maybe owned" sliceno built-inCow<'a, [T]>
    Deref to common typeautomatic (GC reference)Deref trait → &str / &[T]

    Key Insights

  • No allocation by default. In OCaml the GC transparently shares immutable values, so "copy-on-write" is free. In Rust there is no GC; Cow gives you the same guarantee explicitly — Borrowed variant costs nothing.
  • Explicit variant, uniform API. Cow is a plain enum (Borrowed / Owned). You Deref it to &str or &[T] in both cases, so callers never have to match on which variant they received.
  • **to_mut() triggers the lazy clone.** If you hold a Cow::Borrowed and call to_mut(), Rust clones the data into Cow::Owned at that moment — and only at that moment. Subsequent to_mut() calls on the same Owned value are free.
  • Lifetime tracking. Cow<'a, T> carries a lifetime 'a that ties the Borrowed variant to its source. The compiler enforces that the borrow stays valid, something OCaml's GC handles invisibly.
  • Zero-cost abstraction. When the Borrowed path is taken, the entire Cow compiles down to a fat pointer — identical to returning &str directly. There is no runtime overhead from the enum wrapper on the common, unmodified path.
  • When to Use Each Style

    **Use Cow::Borrowed (return the borrow) when:** the data requires no transformation in the common case and you want to avoid heap allocation entirely.

    **Use Cow::Owned (allocate) when:** the function must modify the data — e.g. replacing characters, sorting, escaping — and the original is no longer sufficient.

    **Use to_mut() when:** you start with a borrowed slice but may need to mutate it in-place; to_mut() lazily promotes Borrowed to Owned exactly once.

    Exercises

  • Write escape_html(s: &str) -> Cow<'_, str> that escapes only <, >, and &, returning Borrowed when no escaping is needed.
  • Implement trim_ascii_whitespace(s: &str) -> Cow<'_, str> that returns Borrowed when no trimming is needed.
  • Write a function pipeline that chains multiple Cow-returning functions while avoiding unnecessary allocations: normalize → trim → escape.
  • Open Source Repos