001 — Last Element of a List
Difficulty: ⭐ Category: Lists & Pattern Matching Source: [OCaml.org 99 Problems #1](https://ocaml.org/problems#1) ---Problem Statement
Find the last element of a list. Return `None` if the list is empty.last([1, 2, 3, 4]) → Some(4)
last([]) → None
---
Learning Outcomes
- How Rust slice patterns (`[x]`, `[_, rest @ ..]`) mirror OCaml list patterns
- The difference between borrowing (`&[T]` / `Option<&T>`) and ownership
- Why Rust prefers iteration over recursion for list traversal
- Three idiomatic ways to reach the same result
OCaml Approach
OCaml uses linked lists and recursive pattern matching as a first-class idiom:let rec last = function
| [] -> None
| [x] -> Some x
| _ :: t -> last t
The compiler optimises tail calls, so deep recursion is safe. The standard
library's `List.rev` + head-match is also common.
Rust Approach
Rust represents sequences as slices (`&[T]`), which are contiguous in memory. This enables O(1) random access, so `slice::last()` is the natural first choice. Recursive slice-pattern matching is supported but not tail-call optimised, making it educational rather than production-grade.// O(1) — preferred
pub fn last<T>(list: &[T]) -> Option<&T> {
list.last()
}
---
Key Differences
| Aspect | OCaml | Rust | |
|---|---|---|---|
| Primary sequence type | Linked list | Slice / Vec | |
| Recursive style | Idiomatic, TCO guaranteed | Supported, no TCO | |
| Null safety | `option` type | `Option<T>` | |
| Memory model | GC-managed | Borrow checker enforces lifetimes | |
| Stdlib call | `List.rev lst \ | List.hd_opt` | `slice.last()` |
// Find the last element of a list
// Solution 1: Idiomatic Rust (O(1) slice access)
fn last<T>(list: &[T]) -> Option<&T> {
list.last()
}
// Solution 2: Pattern matching (functional style)
fn last_pattern<T>(list: &[T]) -> Option<&T> {
match list {
[] => None,
[.., last] => Some(last),
}
}
// Solution 3: Recursive (like OCaml, but not idiomatic)
fn last_recursive<T>(list: &[T]) -> Option<&T> {
match list {
[] => None,
[x] => Some(x),
[_, rest @ ..] => last_recursive(rest),
}
}
// Solution 4: Iterator-based (also idiomatic)
fn last_iter<T>(list: &[T]) -> Option<&T> {
list.iter().last()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty() {
let empty: Vec<i32> = vec![];
assert_eq!(last(&empty), None);
}
#[test]
fn test_single() {
assert_eq!(last(&[1]), Some(&1));
}
#[test]
fn test_multiple() {
assert_eq!(last(&[1, 2, 3, 4]), Some(&4));
assert_eq!(last(&["a", "b", "c", "d"]), Some(&"d"));
}
#[test]
fn test_all_implementations() {
let list = vec![1, 2, 3, 4];
assert_eq!(last(&list), last_pattern(&list));
assert_eq!(last(&list), last_recursive(&list));
assert_eq!(last(&list), last_iter(&list));
}
}
fn main() {
println!("last([1,2,3,4]) = {:?}", last(&[1, 2, 3, 4]));
println!("last([]) = {:?}", last::<i32>(&[]));
println!("✓ All tests passed");
}
/* Output:
last([1,2,3,4]) = Some(4)
last([]) = None
✓ All tests passed
*/
(* Find the last element of a list *)
(* Solution 1: Pattern matching (idiomatic OCaml) *)
let rec last = function
| [] -> None
| [x] -> Some x
| _ :: t -> last t
(* Solution 2: Using standard library *)
let last_stdlib lst =
match List.rev lst with
| [] -> None
| h :: _ -> Some h
(* Solution 3: Tail-recursive (better for long lists) *)
let last_tail lst =
let rec aux acc = function
| [] -> acc
| h :: t -> aux (Some h) t
in
aux None lst
(* Tests *)
let () =
assert (last [] = None);
assert (last [1] = Some 1);
assert (last [1; 2; 3; 4] = Some 4);
assert (last ["a"; "b"; "c"; "d"] = Some "d");
print_endline "✓ All tests passed"
(* Output:
✓ All tests passed
*)
📊 Detailed Comparison
OCaml vs Rust: Last Element of a List
Side-by-Side Code
OCaml — idiomatic recursive
🐪 Show OCaml equivalent
let rec last = function
| [] -> None
| [x] -> Some x
| _ :: t -> last tOCaml — stdlib (`List.rev`)
🐪 Show OCaml equivalent
let last_stdlib lst =
match List.rev lst with
| [] -> None
| h :: _ -> Some hOCaml — tail-recursive
🐪 Show OCaml equivalent
let last_tail lst =
let rec aux acc = function
| [] -> acc
| h :: t -> aux (Some h) t
in
aux None lst---
Rust — idiomatic (`slice::last`)
pub fn last<T>(list: &[T]) -> Option<&T> {
list.last() // O(1): reads the final index directly
}Rust — stdlib via iterator
pub fn last_stdlib<T>(list: &[T]) -> Option<&T> {
list.iter().last() // O(n): visits every element
}Rust — recursive slice patterns
pub fn last_recursive<T>(list: &[T]) -> Option<&T> {
match list {
[] => None,
[x] => Some(x),
[_, rest @ ..] => last_recursive(rest),
}
}---
Comparison Table
| Aspect | OCaml | Rust | |
|---|---|---|---|
| Sequence type | `'a list` (linked list, cons cells) | `&[T]` (slice — contiguous memory) | |
| Recursive style | Idiomatic; TCO guaranteed by the compiler | Supported via slice patterns; no TCO | |
| Stdlib one-liner | `List.rev lst \ | List.hd_opt` — O(n) | `slice.last()` — O(1) |
| Null safety | `'a option` | `Option<T>` | |
| Memory management | GC | Borrow checker / lifetimes | |
| Return type | Owned value (`'a option`) | Borrowed reference (`Option<&T>`) |
Type Signatures
🐪 Show OCaml equivalent
(* OCaml — polymorphic, returns owned value *)
val last : 'a list -> 'a option
// Rust — generic, returns borrowed reference
fn last<T>(list: &[T]) -> Option<&T>
// ^^^^ borrows ^ borrows back from inputThe key difference: OCaml's `Some x` copies (or shares via GC) the value. Rust's `Some(&x)` is a reference into the original slice — the caller must keep `list` alive for as long as the returned `Option<&T>` is used.
---
Slice Patterns vs Cons Patterns
| Feature | OCaml | Rust |
|---|---|---|
| Empty | `[]` | `[]` |
| Single element | `[x]` | `[x]` |
| Head + tail | `h :: t` | `[h, rest @ ..]` |
| Last element direct | (requires recursion) | `[.., last]` |
---
5 Takeaways
1. `slice::last()` is O(1) in Rust; the recursive OCaml version is O(n). Slices know their length, so the stdlib call is a single pointer addition.
2. Rust has no guaranteed tail-call optimisation. The idiomatic answer to "tail-recursive traversal" in Rust is an iterator, not explicit recursion.
3. Rust returns a borrow, OCaml returns a value. `Option<&T>` ties the returned reference's lifetime to the slice. This is the borrow checker at work — memory safety without GC.
4. Slice patterns mirror cons patterns almost 1-to-1. Translating `_ :: t -> last t` to `[_, rest @ ..] => last_recursive(rest)` is mechanical, making OCaml → Rust migration of pattern-heavy code readable.
5. Prefer iteration over recursion in Rust. `list.iter().last()` and `list.last()` compile to tight loops or single instructions with no stack growth — the idiomatic preference in a language without TCO.