🦀 Functional Rust

Example 087: Series — Sliding Window

Difficulty: ⭐⭐ Category: String Processing OCaml Source: Classic sliding-window pattern over strings

Problem Statement

Generate all contiguous substrings of length `n` from a string (sliding window), then find the window whose digits have the largest product.

Learning Outcomes

OCaml Approach

OCaml uses `List.init` with `String.sub` to build the list of windows eagerly, then pipes through `List.map` and `List.fold_left` to find the maximum product. `String.fold_left` computes each window's product character by character.

Rust Approach

Rust's standard library provides `slice::windows(n)` directly on `&[u8]`, producing overlapping sub-slices with no allocation. Each sub-slice is re-interpreted as `&str` via `from_utf8`, then mapped to a product via `.chars().map(...).product()`. The max is found with `.max()`.

Key Differences

1. Window generation: OCaml uses `List.init` + `String.sub` (allocates each window); Rust uses `slice::windows()` which yields borrowed sub-slices (zero-copy until `.to_owned()`). 2. Product computation: OCaml's `String.fold_left` with manual accumulator vs. Rust's `.product::<u64>()` iterator method — same semantics, more declarative in Rust. 3. Error handling: OCaml returns `Ok`/`Error` via variant construction; Rust's `Result<u64, String>` is the idiomatic equivalent with `?` propagation available. 4. Integer width: OCaml uses arbitrary-precision integers by default; Rust requires an explicit type (`u64`) to avoid overflow on long digit strings.
pub fn series(n: usize, s: &str) -> Vec<String> {
    if n == 0 {
        return vec![String::new(); s.len() + 1];
    }
    s.as_bytes()
        .windows(n)
        .map(|w| std::str::from_utf8(w).unwrap().to_owned())
        .collect()
}

pub fn series_functional(n: usize, s: &str) -> Vec<String> {
    let len = s.len();
    if n == 0 || n > len {
        if n == 0 {
            return vec![String::new(); len + 1];
        }
        return vec![];
    }
    (0..=len - n).map(|i| s[i..i + n].to_owned()).collect()
}

pub fn largest_product(n: usize, s: &str) -> Result<u64, String> {
    if n == 0 {
        return Ok(1);
    }
    if n > s.len() {
        return Err("span too large".to_string());
    }
    if !s.chars().all(|c| c.is_ascii_digit()) {
        return Err("invalid character".to_string());
    }
    let max = series(n, s)
        .into_iter()
        .map(|sub| {
            sub.chars()
                .map(|c| c as u64 - '0' as u64)
                .product::<u64>()
        })
        .max()
        .unwrap_or(0);
    Ok(max)
}

pub fn largest_product_recursive(n: usize, s: &str) -> Result<u64, String> {
    if n == 0 {
        return Ok(1);
    }
    if n > s.len() {
        return Err("span too large".to_string());
    }
    fn digit_product(s: &str) -> u64 {
        s.chars().map(|c| c as u64 - '0' as u64).product()
    }
    fn go(n: usize, s: &str, best: u64) -> u64 {
        if s.len() < n {
            best
        } else {
            let p = digit_product(&s[..n]);
            go(n, &s[1..], best.max(p))
        }
    }
    Ok(go(n, s, 0))
}

fn main() {
    println!("series(3, \"49142\") = {:?}", series(3, "49142"));
    println!(
        "series_functional(3, \"49142\") = {:?}",
        series_functional(3, "49142")
    );
    println!(
        "largest_product(2, \"0123456789\") = {:?}",
        largest_product(2, "0123456789")
    );
    println!(
        "largest_product(6, \"49142\") = {:?}",
        largest_product(6, "49142")
    );
    println!(
        "largest_product_recursive(2, \"0123456789\") = {:?}",
        largest_product_recursive(2, "0123456789")
    );
}

/* Output:
   series(3, "49142") = ["491", "914", "142"]
   series_functional(3, "49142") = ["491", "914", "142"]
   largest_product(2, "0123456789") = Ok(72)
   largest_product(6, "49142") = Err("span too large")
   largest_product_recursive(2, "0123456789") = Ok(72)
*/
(* Idiomatic OCaml — List.init + String.sub *)
let series n s =
  if n > String.length s then []
  else
    List.init (String.length s - n + 1) (fun i ->
      String.sub s i n
    )

(* Recursive OCaml — explicit tail recursion with accumulator *)
let rec series_rec n s acc i =
  if i + n > String.length s then List.rev acc
  else series_rec n s (String.sub s i n :: acc) (i + 1)

let series_recursive n s =
  if n > String.length s then []
  else series_rec n s [] 0

let largest_product n s =
  if n = 0 then Ok 1
  else if n > String.length s then Error "span too large"
  else
    series n s
    |> List.map (fun sub ->
      String.fold_left (fun acc c ->
        acc * (Char.code c - Char.code '0')
      ) 1 sub
    )
    |> List.fold_left max 0
    |> fun m -> Ok m

let () =
  assert (series 3 "49142" = ["491"; "914"; "142"]);
  assert (series 6 "49142" = []);
  assert (series_recursive 3 "49142" = ["491"; "914"; "142"]);
  assert (largest_product 0 "12345" = Ok 1);
  assert (largest_product 2 "0123456789" = Ok 72);
  assert (largest_product 6 "49142" = Error "span too large");
  List.iter (Printf.printf "%s ") (series 3 "49142");
  print_newline ();
  (match largest_product 2 "0123456789" with
  | Ok n -> Printf.printf "Largest: %d\n" n
  | Error e -> Printf.printf "Error: %s\n" e);
  print_endline "ok"

📊 Detailed Comparison

OCaml vs Rust: Series — Sliding Window

Side-by-Side Code

OCaml

🐪 Show OCaml equivalent
let series n s =
if n > String.length s then []
else
 List.init (String.length s - n + 1) (fun i ->
   String.sub s i n
 )

let largest_product n s =
if n = 0 then Ok 1
else if n > String.length s then Error "span too large"
else
 series n s
 |> List.map (fun sub ->
   String.fold_left (fun acc c ->
     acc * (Char.code c - Char.code '0')
   ) 1 sub
 )
 |> List.fold_left max 0
 |> fun m -> Ok m

Rust (idiomatic)

pub fn series(n: usize, s: &str) -> Vec<String> {
 if n == 0 { return vec![String::new(); s.len() + 1]; }
 s.as_bytes()
     .windows(n)
     .map(|w| std::str::from_utf8(w).unwrap().to_owned())
     .collect()
}

pub fn largest_product(n: usize, s: &str) -> Result<u64, String> {
 if n == 0 { return Ok(1); }
 if n > s.len() { return Err("span too large".to_string()); }
 let max = series(n, s)
     .into_iter()
     .map(|sub| sub.chars().map(|c| c as u64 - '0' as u64).product::<u64>())
     .max()
     .unwrap_or(0);
 Ok(max)
}

Rust (functional/recursive)

pub fn largest_product_recursive(n: usize, s: &str) -> Result<u64, String> {
 if n == 0 { return Ok(1); }
 if n > s.len() { return Err("span too large".to_string()); }
 fn digit_product(s: &str) -> u64 {
     s.chars().map(|c| c as u64 - '0' as u64).product()
 }
 fn go(n: usize, s: &str, best: u64) -> u64 {
     if s.len() < n { best }
     else {
         let p = digit_product(&s[..n]);
         go(n, &s[1..], best.max(p))
     }
 }
 Ok(go(n, s, 0))
}

Type Signatures

ConceptOCamlRust
Sliding windows`val series : int -> string -> string list``fn series(n: usize, s: &str) -> Vec<String>`
Product with error`val largest_product : int -> string -> (int, string) result``fn largest_product(n: usize, s: &str) -> Result<u64, String>`
Fold over chars`String.fold_left : ('a -> char -> 'a) -> 'a -> string -> 'a``.chars().fold(init, f)` or `.product()`
Max of list`List.fold_left max 0``.max().unwrap_or(0)`
Optional integer`'a option``Option<T>`

Key Insights

1. `slice::windows()` is the idiomatic Rust primitive. OCaml has no built-in sliding-window for strings; you reconstruct it with `List.init` + `String.sub`. Rust's `slice::windows(n)` is provided by the standard library and produces borrowed sub-slices with zero additional allocation, making it both ergonomic and efficient.

2. Byte slice vs. character slice. Rust strings are UTF-8, so `&str` has no `O(1)` indexing. Working via `as_bytes()` is safe here because ASCII digit strings are single-byte, and `from_utf8` re-borrows the window as `&str` without copying.

3. `product()` replaces `fold_left () 1`. OCaml's `String.fold_left (fun acc c -> acc digit_of c) 1 sub` maps directly to `.chars().map(digit_of).product::<u64>()` in Rust. The `product` iterator method is the exact Rust idiom for multiplicative folds.

4. Explicit integer width matters. OCaml integers are arbitrary precision (via the runtime); Rust requires you to pick a type. `u64` safely holds the product of up to 19 single-digit values before overflow, which covers all practical inputs for this problem.

5. Recursive tail call with accumulator. The recursive Rust version passes a `best` accumulator through `go(n, &s[1..], best.max(p))`, consuming one character per recursive call — exactly the OCaml `List.fold_left` pattern translated to explicit recursion on string slices.

When to Use Each Style

Use idiomatic Rust when: you want readable, allocation-light code that composes naturally with the rest of the iterator pipeline. `windows()` + `map()` + `max()` reads as a single declarative query.

Use recursive Rust when: teaching the OCaml parallel explicitly, or when the algorithm is naturally expressed as "process head, recurse on tail" and you want to avoid collecting an intermediate `Vec`.