Example 087: Series — Sliding Window
Difficulty: ⭐⭐ Category: String Processing OCaml Source: Classic sliding-window pattern over stringsProblem 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
- How Rust's `slice::windows()` provides zero-copy sliding windows over byte slices
- Converting byte windows back to `&str` with `std::str::from_utf8`
- Chaining iterator adaptors (`windows`, `map`, `max`) as a direct parallel to OCaml's `List.init` + `List.map` + `List.fold_left`
- Using `Result<T, String>` for domain errors vs. panicking
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 mRust (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
| Concept | OCaml | Rust |
|---|---|---|
| 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`.