Example 002: Last Two Elements
Difficulty: โญ Category: Lists & Pattern Matching OCaml Source: OCaml.org 99 Problems #2Problem Statement
Find the last two elements of a list. Return them as a pair (tuple), or `None` if the list has fewer than two elements.Learning Outcomes
- Pattern matching on lists/slices with multiple cases
- Returning tuples (pairs) from functions
- Understanding `Option<(T, T)>` vs OCaml's `option of (a * a)`
- Using `windows()` for sliding-window iteration
- Recursive slice destructuring with `[_, rest @ ..]`
OCaml Approach
OCaml uses nested pattern matching: `[] | [_] -> None`, `[x; y] -> Some (x, y)`, and `_ :: t -> last_two t`. The recursive structure naturally peels off the head until two elements remain.Rust Approach
Three approaches: (1) direct length-based indexing for O(1) access, (2) recursive slice patterns mirroring OCaml, and (3) `windows(2).last()` for an iterator-based solution. All return references (`&T`) to avoid cloning.Key Differences
1. Return type: OCaml returns `Some (x, y)` (owned copy); Rust returns `Option<(&T, &T)>` (borrowed references) 2. Tuple syntax: OCaml `(x, y)` vs Rust `(x, y)` โ similar, but Rust tuples can hold references 3. Slice access: Rust slices are contiguous memory โ O(1) indexing; OCaml lists are linked โ O(n) traversal 4. Windows iterator: Rust's `windows(n)` has no direct OCaml equivalent โ it exploits contiguous memory 5. Exhaustive matching: Both languages require exhaustive patterns, but Rust's slice patterns (`[_, rest @ ..]`) are less common than OCaml's list patterns// Find the last two elements of a list
// Idiomatic Rust with slice patterns
fn last_two<T>(list: &[T]) -> Option<(&T, &T)> {
match list {
[.., a, b] => Some((a, b)),
_ => None,
}
}
// Alternative: using split_last
fn last_two_split<T>(list: &[T]) -> Option<(&T, &T)> {
let (last, rest) = list.split_last()?;
let (second_last, _) = rest.split_last()?;
Some((second_last, last))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cases() {
assert_eq!(last_two::<i32>(&[]), None);
assert_eq!(last_two(&[1]), None);
assert_eq!(last_two(&[1, 2]), Some((&1, &2)));
assert_eq!(last_two(&[1, 2, 3, 4]), Some((&3, &4)));
}
}
fn main() {
println!("last_two([1,2,3,4]) = {:?}", last_two(&[1, 2, 3, 4]));
println!("โ Rust tests passed");
}
(* Find the last two elements of a list *)
let rec last_two = function
| [] | [_] -> None
| [x; y] -> Some (x, y)
| _ :: t -> last_two t
(* Tests *)
let () =
assert (last_two [] = None);
assert (last_two [1] = None);
assert (last_two [1; 2] = Some (1, 2));
assert (last_two [1; 2; 3; 4] = Some (3, 4));
print_endline "โ OCaml tests passed"
๐ Detailed Comparison
Comparison: Last Two Elements
OCaml
๐ช Show OCaml equivalent
let rec last_two = function
| [] | [_] -> None
| [x; y] -> Some (x, y)
| _ :: t -> last_two t
Rust โ Idiomatic (direct indexing)
pub fn last_two<T>(slice: &[T]) -> Option<(&T, &T)> {
let len = slice.len();
if len < 2 { None }
else { Some((&slice[len - 2], &slice[len - 1])) }
}Rust โ Functional (recursive)
pub fn last_two_recursive<T>(slice: &[T]) -> Option<(&T, &T)> {
match slice {
[] | [_] => None,
[x, y] => Some((x, y)),
[_, rest @ ..] => last_two_recursive(rest),
}
}Rust โ Iterator (windows)
pub fn last_two_windows<T>(slice: &[T]) -> Option<(&T, &T)> {
slice.windows(2).last().map(|w| (&w[0], &w[1]))
}Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Data structure | `'a list` (linked list) | `&[T]` (slice / contiguous) |
| Return type | `('a * 'a) option` | `Option<(&T, &T)>` |
| Ownership | Copies values into tuple | Borrows references |
| Pattern syntax | `[x; y]` | `[x, y]` |
| Complexity | O(n) always | O(1) idiomatic, O(n) recursive |
Type Signatures
- OCaml: `val last_two : 'a list -> ('a * 'a) option`
- Rust: `fn last_two<T>(slice: &[T]) -> Option<(&T, &T)>`
Takeaways
1. Rust's contiguous memory layout enables O(1) access to the last two elements โ OCaml's linked list requires full traversal 2. Rust returns borrowed references (`&T`) instead of copies, avoiding allocation for large types 3. Slice pattern matching in Rust (`[x, y]`, `[_, rest @ ..]`) closely mirrors OCaml's list patterns 4. The `windows()` iterator is uniquely Rust โ it exploits contiguous memory for sliding-window views 5. Both languages use `Option`/`option` to safely handle the "not enough elements" case without exceptions