Last Two Elements
Tutorial Video
Text description (accessibility)
This video demonstrates the "Last Two Elements" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Lists, Pattern Matching. Find the last two elements of a list. Key difference from OCaml: 1. **Return type:** OCaml returns `Some (x, y)` (owned copy); Rust returns `Option<(&T, &T)>` (borrowed references)
Tutorial
The Problem
Find the last two elements of a list. Return them as a pair (tuple), or None if the list has fewer than two elements.
The problem of finding trailing elements arises in streaming systems (keep the last k log entries), undo systems (last two states for delta computation), and sliding-window algorithms. The key insight is that slices in Rust provide O(1) random access, so taking the last two requires no traversal — unlike linked lists in OCaml where you must walk to the end.
🎯 Learning Outcomes
Option<(T, T)> vs OCaml's option of (a * a)windows() for sliding-window iteration[_, rest @ ..]🦀 The Rust Way
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.
Code Example
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])) }
}Key Differences
Some (x, y) (owned copy); Rust returns Option<(&T, &T)> (borrowed references)(x, y) vs Rust (x, y) — similar, but Rust tuples can hold referenceswindows(n) has no direct OCaml equivalent — it exploits contiguous memory[_, rest @ ..]) are less common than OCaml's list patternsslice.len() + direct indexing is O(1); OCaml's recursive version is O(n) because linked lists don't store their length or tail pointer.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.
The recursive OCaml version let rec last_two = function [] | [_] -> None | [x; y] -> Some (x, y) | _ :: t -> last_two t is idiomatic and clear, but O(n) because it must traverse the entire linked list. OCaml's List module does not cache the length or provide direct indexing, so walking to the end is unavoidable.
Full Source
#![allow(clippy::all)]
// Find the last two elements of a list, returning them as a tuple.
// ---------------------------------------------------------------------------
// Approach 1: Idiomatic Rust — slice pattern matching
// ---------------------------------------------------------------------------
// Slices in Rust support pattern matching similar to OCaml's list patterns.
// We borrow the slice (`&[T]`) to avoid taking ownership.
pub fn last_two<T>(slice: &[T]) -> Option<(&T, &T)> {
let len = slice.len();
if len < 2 {
None
} else {
// Direct indexing — O(1) since slices are contiguous memory
Some((&slice[len - 2], &slice[len - 1]))
}
}
// ---------------------------------------------------------------------------
// Approach 2: Functional — recursive, mirrors the OCaml version
// ---------------------------------------------------------------------------
// Uses slice patterns `[x, y]` and `[_, rest @ ..]` for destructuring.
// Note: Rust doesn't guarantee TCO, but this is educational.
pub fn last_two_recursive<T>(slice: &[T]) -> Option<(&T, &T)> {
match slice {
// Empty or single element — no pair exists
[] | [_] => None,
// Exactly two elements — base case
[x, y] => Some((x, y)),
// More than two — recurse on tail (skip first element)
[_, rest @ ..] => last_two_recursive(rest),
}
}
// ---------------------------------------------------------------------------
// Approach 3: Iterator-based — using windows
// ---------------------------------------------------------------------------
// `windows(2)` gives sliding pairs; we take the last one.
pub fn last_two_windows<T>(slice: &[T]) -> Option<(&T, &T)> {
// windows(2) yields &[T] slices of length 2
slice.windows(2).last().map(|w| (&w[0], &w[1]))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty() {
assert_eq!(last_two::<i32>(&[]), None);
assert_eq!(last_two_recursive::<i32>(&[]), None);
assert_eq!(last_two_windows::<i32>(&[]), None);
}
#[test]
fn test_single() {
assert_eq!(last_two(&[1]), None);
assert_eq!(last_two_recursive(&[1]), None);
assert_eq!(last_two_windows(&[1]), None);
}
#[test]
fn test_two_elements() {
assert_eq!(last_two(&[1, 2]), Some((&1, &2)));
assert_eq!(last_two_recursive(&[1, 2]), Some((&1, &2)));
assert_eq!(last_two_windows(&[1, 2]), Some((&1, &2)));
}
#[test]
fn test_multiple() {
assert_eq!(last_two(&[1, 2, 3, 4]), Some((&3, &4)));
assert_eq!(last_two_recursive(&[1, 2, 3, 4]), Some((&3, &4)));
assert_eq!(last_two_windows(&[1, 2, 3, 4]), Some((&3, &4)));
}
#[test]
fn test_strings() {
let v = ["a", "b", "c", "d"];
assert_eq!(last_two(&v), Some((&"c", &"d")));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty() {
assert_eq!(last_two::<i32>(&[]), None);
assert_eq!(last_two_recursive::<i32>(&[]), None);
assert_eq!(last_two_windows::<i32>(&[]), None);
}
#[test]
fn test_single() {
assert_eq!(last_two(&[1]), None);
assert_eq!(last_two_recursive(&[1]), None);
assert_eq!(last_two_windows(&[1]), None);
}
#[test]
fn test_two_elements() {
assert_eq!(last_two(&[1, 2]), Some((&1, &2)));
assert_eq!(last_two_recursive(&[1, 2]), Some((&1, &2)));
assert_eq!(last_two_windows(&[1, 2]), Some((&1, &2)));
}
#[test]
fn test_multiple() {
assert_eq!(last_two(&[1, 2, 3, 4]), Some((&3, &4)));
assert_eq!(last_two_recursive(&[1, 2, 3, 4]), Some((&3, &4)));
assert_eq!(last_two_windows(&[1, 2, 3, 4]), Some((&3, &4)));
}
#[test]
fn test_strings() {
let v = ["a", "b", "c", "d"];
assert_eq!(last_two(&v), Some((&"c", &"d")));
}
}
Deep Comparison
Comparison: Last Two Elements
OCaml
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
val last_two : 'a list -> ('a * 'a) optionfn last_two<T>(slice: &[T]) -> Option<(&T, &T)>Takeaways
&T) instead of copies, avoiding allocation for large types[x, y], [_, rest @ ..]) closely mirrors OCaml's list patternswindows() iterator is uniquely Rust — it exploits contiguous memory for sliding-window viewsOption/option to safely handle the "not enough elements" case without exceptionsExercises
last_n that returns the last n elements of a slice as an Option<&[T]>, returning None if the slice is shorter than n.last_two_by that returns the last two elements of a slice satisfying a predicate, using only iterator combinators.sliding_last that yields all consecutive windows of size k from the end of a list, collecting them into a Vec<Vec<T>>.last_two to be generic over T: Clone, returning Option<(T, T)> (owned values instead of references). When is this better than returning references?last_two (direct indexing), last_two_recursive, and last_two_windows for slices of 10, 1000, and 1,000,000 elements.