001 — Last Element of a List
Tutorial Video
Text description (accessibility)
This video demonstrates the "001 — Last Element of a List" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Find the last element of a list. Key difference from OCaml: | Aspect | OCaml | Rust |
Tutorial
The Problem
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
[x], [_, rest @ ..]) mirror OCaml list patterns&[T] / Option<&T>) and ownership🦀 The Rust Way
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()
}
Code Example
pub fn last<T>(list: &[T]) -> Option<&T> {
list.last() // O(1): reads the final index directly
}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() |
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.
Full Source
#![allow(clippy::all)]
// Find the last element of a list
//
// Three implementations showing different Rust idioms.
// All functions borrow the slice (&[T]) — no ownership transfer,
// and return Option<&T>, borrowing from the input.
// Solution 1: Idiomatic — delegate to the standard library.
// `slice::last()` is O(1): it reads the final index directly.
// The returned reference borrows from `list`, so the caller cannot
// mutate or drop `list` while the reference is live.
pub fn last<T>(list: &[T]) -> Option<&T> {
list.last()
}
// Solution 2: Stdlib-based via iterator.
// `Iterator::last()` consumes the iterator in O(n) time.
// Useful as a reminder that iterators are lazy; here every element
// is visited just to reach the end — prefer `slice::last()` in practice.
pub fn last_stdlib<T>(list: &[T]) -> Option<&T> {
list.iter().last()
}
// Solution 3: Recursive pattern matching (closest to the OCaml original).
// Rust slice patterns let us destructure `[head, rest @ ..]` the same way
// OCaml matches `_ :: t`. This is NOT tail-call optimised by the compiler,
// so very long lists could overflow the stack — idiomatic Rust prefers
// iteration over recursion for this reason.
pub fn last_recursive<T>(list: &[T]) -> Option<&T> {
match list {
// Empty slice — no element to return.
[] => None,
// Single element — this IS the last one.
[x] => Some(x),
// Head + tail: discard head, recurse into the tail.
// `rest` is a &[T] sub-slice; no allocation occurs.
[_, rest @ ..] => last_recursive(rest),
}
}
#[cfg(test)]
mod tests {
use super::*;
// Empty list: every implementation must return None.
#[test]
fn test_empty() {
let empty: &[i32] = &[];
assert_eq!(last(empty), None);
assert_eq!(last_stdlib(empty), None);
assert_eq!(last_recursive(empty), None);
}
// Single element: the only element is also the last.
#[test]
fn test_single() {
assert_eq!(last(&[42]), Some(&42));
assert_eq!(last_stdlib(&[42]), Some(&42));
assert_eq!(last_recursive(&[42]), Some(&42));
}
// Multiple integers: last element is 4.
#[test]
fn test_multiple_integers() {
let list = [1, 2, 3, 4];
assert_eq!(last(&list), Some(&4));
assert_eq!(last_stdlib(&list), Some(&4));
assert_eq!(last_recursive(&list), Some(&4));
}
// Multiple strings: mirrors the OCaml test case exactly.
#[test]
fn test_multiple_strings() {
let list = ["a", "b", "c", "d"];
assert_eq!(last(&list), Some(&"d"));
assert_eq!(last_stdlib(&list), Some(&"d"));
assert_eq!(last_recursive(&list), Some(&"d"));
}
// All three implementations must agree on the same result.
#[test]
fn test_all_implementations_agree() {
let list = [10, 20, 30, 40, 50];
let expected = Some(&50);
assert_eq!(last(&list), expected);
assert_eq!(last_stdlib(&list), expected);
assert_eq!(last_recursive(&list), expected);
}
}#[cfg(test)]
mod tests {
use super::*;
// Empty list: every implementation must return None.
#[test]
fn test_empty() {
let empty: &[i32] = &[];
assert_eq!(last(empty), None);
assert_eq!(last_stdlib(empty), None);
assert_eq!(last_recursive(empty), None);
}
// Single element: the only element is also the last.
#[test]
fn test_single() {
assert_eq!(last(&[42]), Some(&42));
assert_eq!(last_stdlib(&[42]), Some(&42));
assert_eq!(last_recursive(&[42]), Some(&42));
}
// Multiple integers: last element is 4.
#[test]
fn test_multiple_integers() {
let list = [1, 2, 3, 4];
assert_eq!(last(&list), Some(&4));
assert_eq!(last_stdlib(&list), Some(&4));
assert_eq!(last_recursive(&list), Some(&4));
}
// Multiple strings: mirrors the OCaml test case exactly.
#[test]
fn test_multiple_strings() {
let list = ["a", "b", "c", "d"];
assert_eq!(last(&list), Some(&"d"));
assert_eq!(last_stdlib(&list), Some(&"d"));
assert_eq!(last_recursive(&list), Some(&"d"));
}
// All three implementations must agree on the same result.
#[test]
fn test_all_implementations_agree() {
let list = [10, 20, 30, 40, 50];
let expected = Some(&50);
assert_eq!(last(&list), expected);
assert_eq!(last_stdlib(&list), expected);
assert_eq!(last_recursive(&list), expected);
}
}
Deep Comparison
OCaml vs Rust: Last Element of a List
Side-by-Side Code
OCaml — idiomatic recursive
let rec last = function
| [] -> None
| [x] -> Some x
| _ :: t -> last t
OCaml — stdlib (List.rev)
let last_stdlib lst =
match List.rev lst with
| [] -> None
| h :: _ -> Some h
OCaml — tail-recursive
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
(* 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 input
The 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] |
Rust slice patterns can match from either end, which OCaml cons patterns
cannot — [.., last] directly binds the final element with no recursion.
5 Takeaways
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.
The idiomatic answer to "tail-recursive traversal" in Rust is an iterator, not explicit recursion.
Option<&T> ties the returned reference's lifetime to the slice. This is
the borrow checker at work — memory safety without GC.
Translating _ :: t -> last t to [_, rest @ ..] => last_recursive(rest)
is mechanical, making OCaml → Rust migration of pattern-heavy code readable.
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.
Exercises
second_to_last that returns the second-to-last element of a list as an Option.last_n that returns the last n elements of a list as a Vec, returning an empty vec if the list is shorter.last_element into a last_by function that accepts a predicate and returns the last element satisfying it, then use it to find the last even number in a list of integers.