๐Ÿฆ€ Functional Rust

113: String vs &str

Difficulty: 2 Level: Intermediate `String` is an owned, heap-allocated string you can grow; `&str` is a borrowed view into any string data โ€” use `&str` in function parameters to accept both without allocating.

The Problem This Solves

In C, strings are `char*` โ€” a raw pointer. Owned, borrowed, literal, stack-allocated โ€” the type doesn't tell you. You discover which it is when you try to `free()` the wrong one and the program crashes, or when you modify a string literal and get a segfault. In Java, `String` is always heap-allocated and immutable. Simple โ€” but you can't have a "view into part of a string" without copying. Every substring allocates. String-heavy code in Java has serious GC pressure. OCaml's `string` type is similarly a single concept: immutable, GC-managed. No distinction between owned and borrowed. Clean, but at the cost of GC overhead on all strings. Rust has two types because they solve different problems. `String` is owned heap-allocated data โ€” you can push, truncate, and pass it around while keeping ownership clear. `&str` is a borrowed slice โ€” a pointer and a length pointing into any string data (a `String`, a string literal baked into the binary, or a substring). No allocation. Pass `&str` to functions and you never force the caller to allocate.

The Intuition

`String` owns the text on the heap; `&str` borrows a view into any existing text โ€” using `&str` in function signatures lets callers pass string literals, slices, or `String` references without forcing an allocation.

How It Works in Rust

// String: owned, heap-allocated, growable
let mut owned: String = String::from("hello");
owned.push_str(", world"); // grows in-place
owned.push('!');
println!("{}", owned); // "hello, world!"

// &str: borrowed view โ€” no allocation
let borrowed: &str = "hello, world!"; // points into binary (static lifetime)
let slice: &str = &owned[0..5];       // points into the String

// Function that accepts both &str and &String
// BAD: fn greet(name: &String) โ€” forces callers to have a String
// GOOD: fn greet(name: &str) โ€” works with literals AND String
fn greet(name: &str) {
 println!("Hello, {}!", name);
}

fn demo() {
 let owned = String::from("Alice");
 greet(&owned);        // coerces &String to &str automatically
 greet("Bob");         // string literal is already &'static str
 greet(&owned[0..3]);  // slice of String โ€” also &str
}

// Converting between them
let s: String = "hello".to_string();    // &str โ†’ String (allocates)
let s: String = String::from("hello");  // same
let r: &str = &s;                       // String โ†’ &str (no allocation)
let r: &str = s.as_str();               // explicit

// String owns its buffer; dropping String frees the memory
// &str borrows โ€” it can't outlive the data it points to
fn returns_str() -> &'static str {
 "static lifetime โ€” baked into binary"
}

// This would be a compile error:
// fn returns_local_str() -> &str {
//     let s = String::from("temporary");
//     &s // ERROR: s is dropped, &s would dangle
// }

What This Unlocks

Key Differences

ConceptOCamlRust
String typeSingle `string` (immutable, GC)`String` (owned) and `&str` (borrowed)
Substring`String.sub` โ€” always copies`&s[start..end]` โ€” zero-copy view
String literal type`string``&'static str`
Function parameter`string` โ€” always owned refPrefer `&str` โ€” accepts all string kinds
MutabilityImmutable (use `Bytes` for mutable)`String` is mutable; `&str` is read-only
// Example 113: String vs &str
//
// String: owned, heap-allocated, growable, mutable
// &str: borrowed slice, usually a view into a String or string literal

// Approach 1: String creation and conversions
fn approach1() {
    // String literals are &'static str
    let literal: &str = "hello";
    
    // Convert to owned String
    let mut owned: String = String::from("hello");
    owned.push_str(" world");
    
    // &str from String
    let slice: &str = &owned;
    let upper = slice.to_uppercase(); // returns new String
    
    assert_eq!(upper, "HELLO WORLD");
    println!("Literal: {}, Owned: {}, Upper: {}", literal, owned, upper);
}

// Approach 2: Functions that accept both
fn first_word(s: &str) -> &str {
    s.split(',').next().unwrap_or(s).trim()
}

fn char_count(s: &str) -> usize {
    s.chars().count()
}

fn approach2() {
    // Works with &str
    let word = first_word("hello, world!");
    assert_eq!(word, "hello");
    
    // Works with &String (auto-deref)
    let owned = String::from("goodbye, moon!");
    let word2 = first_word(&owned);
    assert_eq!(word2, "goodbye");
    
    println!("word: {}, word2: {}, chars: {}", word, word2, char_count(word));
}

// Approach 3: Building strings efficiently
fn approach3() {
    // With push/push_str
    let mut s = String::with_capacity(50);
    s.push_str("Hello");
    s.push(' ');
    s.push_str("World");
    
    // With format!
    let formatted = format!("{} - {}", s, 2024);
    
    // With collect
    let collected: String = "hello".chars().map(|c| c.to_uppercase().next().unwrap()).collect();
    
    // With join
    let joined = ["one", "two", "three"].join(", ");
    
    assert_eq!(s, "Hello World");
    assert_eq!(collected, "HELLO");
    println!("{} | {} | {} | {}", s, formatted, collected, joined);
}

fn main() {
    println!("=== Approach 1: String vs &str ===");
    approach1();
    println!("\n=== Approach 2: Accept Both ===");
    approach2();
    println!("\n=== Approach 3: Building Strings ===");
    approach3();
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_str_to_string() {
        let s: &str = "hello";
        let owned: String = s.to_string();
        assert_eq!(s, owned.as_str());
    }

    #[test]
    fn test_string_slicing() {
        let s = String::from("hello world");
        let hello: &str = &s[..5];
        assert_eq!(hello, "hello");
    }

    #[test]
    fn test_first_word() {
        assert_eq!(first_word("a, b"), "a");
        assert_eq!(first_word("single"), "single");
    }

    #[test]
    fn test_string_building() {
        let mut s = String::new();
        s.push_str("abc");
        s.push('d');
        assert_eq!(s, "abcd");
        assert_eq!(s.len(), 4);
        assert_eq!(s.capacity() >= 4, true);
    }

    #[test]
    fn test_utf8() {
        let s = "hรฉllo";
        assert_eq!(s.len(), 6);      // bytes
        assert_eq!(s.chars().count(), 5); // characters
    }
}
(* Example 113: String vs &str โ€” OCaml string โ†’ Rust Two String Types *)

(* OCaml has one string type. Rust has two: String (owned, heap) and &str (borrowed slice). *)

(* Approach 1: String creation and manipulation *)
let approach1 () =
  let s = "hello" in
  let s2 = s ^ " world" in
  let upper = String.uppercase_ascii s2 in
  assert (upper = "HELLO WORLD");
  Printf.printf "Original: %s, Upper: %s\n" s2 upper

(* Approach 2: Substring operations *)
let approach2 () =
  let s = "hello, world!" in
  let sub = String.sub s 7 5 in
  let first_word = match String.index_opt s ',' with
    | Some i -> String.sub s 0 i
    | None -> s in
  assert (sub = "world");
  assert (first_word = "hello");
  Printf.printf "Sub: %s, First word: %s\n" sub first_word

(* Approach 3: String as bytes *)
let approach3 () =
  let s = "Rust" in
  let bytes = Bytes.of_string s in
  Bytes.set bytes 0 'r';
  let modified = Bytes.to_string bytes in
  assert (modified = "rust");
  Printf.printf "Modified: %s\n" modified

let () =
  approach1 ();
  approach2 ();
  approach3 ();
  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Comparison: String vs &str

String Creation

OCaml:

๐Ÿช Show OCaml equivalent
let s = "hello"           (* string literal *)
let s2 = s ^ " world"     (* concatenation โ†’ new string *)

Rust:

let s: &str = "hello";                    // &'static str
let s2: String = format!("{} world", s);  // owned String
let s3 = String::from("hello");           // also owned

Function Parameters

OCaml:

๐Ÿช Show OCaml equivalent
let greet name = Printf.printf "Hello, %s!\n" name
let () = greet "world"  (* just one string type *)

Rust:

fn greet(name: &str) { println!("Hello, {}!", name); }
greet("world");                     // &str โœ“
greet(&String::from("world"));     // &String auto-derefs to &str โœ“

Mutability

OCaml:

๐Ÿช Show OCaml equivalent
let bytes = Bytes.of_string "hello" in
Bytes.set bytes 0 'H';
Bytes.to_string bytes  (* "Hello" *)

Rust:

let mut s = String::from("hello");
s.push_str(" world");    // mutate in-place
s.replace_range(0..1, "H");
assert_eq!(s, "Hello world");