ExamplesBy LevelBy TopicLearning Paths
001 Fundamental

001 — Last Element of a List

Functional Programming

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

  • • 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

  • 🦀 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

    AspectOCamlRust
    Primary sequence typeLinked listSlice / Vec
    Recursive styleIdiomatic, TCO guaranteedSupported, no TCO
    Null safetyoption typeOption<T>
    Memory modelGC-managedBorrow checker enforces lifetimes
    Stdlib callList.rev lst \| List.hd_optslice.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);
        }
    }
    ✓ Tests Rust test suite
    #[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

    AspectOCamlRust
    Sequence type'a list (linked list, cons cells)&[T] (slice — contiguous memory)
    Recursive styleIdiomatic; TCO guaranteed by the compilerSupported via slice patterns; no TCO
    Stdlib one-linerList.rev lst \| List.hd_opt — O(n)slice.last() — O(1)
    Null safety'a optionOption<T>
    Memory managementGCBorrow checker / lifetimes
    Return typeOwned 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

    FeatureOCamlRust
    Empty[][]
    Single element[x][x]
    Head + tailh :: 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.

  • Rust has no guaranteed tail-call optimisation.
  • The idiomatic answer to "tail-recursive traversal" in Rust is an iterator, not explicit recursion.

  • 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.

  • 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.

  • 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.

    Exercises

  • Implement second_to_last that returns the second-to-last element of a list as an Option.
  • Implement last_n that returns the last n elements of a list as a Vec, returning an empty vec if the list is shorter.
  • Generalize 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.
  • Open Source Repos