ExamplesBy LevelBy TopicLearning Paths
1218 Intermediate GitHub

List Sort — Sort with Custom Comparator

ListsHOF

Tutorial

The Problem

Given a list of strings (e.g. ["banana"; "apple"; "cherry"; "date"]), produce new lists sorted (a) alphabetically and (b) by length, using a caller-supplied comparator. The original list must not be mutated.

🎯 Learning Outcomes

  • • Translating List.sort : ('a -> 'a -> int) -> 'a list -> 'a list into Rust's slice sort family (sort_by, sort_by_key).
  • • How OCaml's int-returning comparator becomes Rust's Ordering enum (Less / Equal / Greater).
  • • Why sort_by_key is the preferred form when the comparator is pure key extraction — it is O(n) keys + O(n log n) comparisons, not O(n log n) key recomputations.
  • • Stability: both List.sort (OCaml) and slice::sort_by (Rust) are stable — a tie-break test pins this down.
  • • Returning a new Vec<T> (via to_vec() + in-place sort) to mirror OCaml's non-mutating semantics.
  • 🦀 The Rust Way

    Rust's sort family lives on slices and sorts in place. To mirror OCaml's "returns a new list" contract, we clone/to_vec the input first, then call sort_by (for a full comparator) or sort_by_key (for key-based comparators — the idiomatic shortcut). The closure returns a std::cmp::Ordering value built by chaining .cmp() / .then_with().

    Code Example

    use std::cmp::Ordering;
    
    pub fn sort_by_comparator<T, F>(items: &[T], cmp: F) -> Vec<T>
    where
        T: Clone,
        F: FnMut(&T, &T) -> Ordering,
    {
        let mut out = items.to_vec();
        out.sort_by(cmp);
        out
    }
    
    pub fn sort_alphabetical(words: &[String]) -> Vec<String> {
        sort_by_comparator(words, |a, b| a.cmp(b))
    }

    Key Differences

  • Return type of the comparator: OCaml uses int (sign matters, magnitude is ignored); Rust uses the Ordering enum — type-safe but less flexible when subtracting numeric keys.
  • In-place vs pure: OCaml's List.sort is pure (returns a new list); Rust's sort_by mutates an owned Vec. You pay one clone per call to preserve the OCaml contract.
  • Key extraction shortcut: Rust has sort_by_key / sort_by_cached_key — OCaml has no direct equivalent and re-extracts the key inside the lambda every comparison.
  • Stability: both are stable. Rust's sort_unstable_by is faster but loses the tie-break guarantee — read the method name carefully.
  • OCaml Approach

    List.sort takes a three-way comparator returning a negative / zero / positive int and returns a brand-new sorted list, leaving the input untouched. Common comparators (String.compare, compare) are already three-way functions, so they can be passed by name. Custom orderings are built as lambdas: fun a b -> compare (f a) (f b).

    Full Source

    #![allow(dead_code)]
    //! `List.sort` — sort a list with a custom comparator.
    //!
    //! OCaml's `List.sort : ('a -> 'a -> int) -> 'a list -> 'a list` takes a
    //! three-way comparator (negative / zero / positive) and returns a **new**
    //! sorted list. Rust's slice API is a close match but differs in two ways:
    //!
    //!   * the comparator returns `std::cmp::Ordering` instead of an `int`,
    //!   * slice `sort_by` sorts **in place** on an owned `Vec<T>`; to mirror
    //!     OCaml's "return a new list" shape, we clone the input before sorting.
    //!
    //! Three translations are shown:
    //!   * `sort_by_comparator` — direct OCaml parallel with a generic `Fn`,
    //!   * `sort_by_key_fn` — the idiomatic Rust shortcut when the comparator
    //!     reduces to "compare some key extracted from each element",
    //!   * specialised helpers `sort_alphabetical` / `sort_by_length` that match
    //!     the OCaml example exactly.
    
    use std::cmp::Ordering;
    
    /// Direct OCaml parallel: take a three-way comparator and return a new
    /// sorted `Vec<T>`. The input slice is borrowed; the output is owned.
    ///
    /// `T: Clone` is required because we copy the input elements into a fresh
    /// `Vec` before sorting — OCaml's `List.sort` likewise returns a new list
    /// without touching the original.
    pub fn sort_by_comparator<T, F>(items: &[T], cmp: F) -> Vec<T>
    where
        T: Clone,
        F: FnMut(&T, &T) -> Ordering,
    {
        let mut out = items.to_vec();
        out.sort_by(cmp);
        out
    }
    
    /// Idiomatic shortcut when the comparator is "compare some key of each
    /// element". `K: Ord` replaces the hand-written `Ordering`, and the key
    /// function is evaluated once per element (sort is `O(n log n)` comparisons,
    /// but key extraction is `O(n)`).
    pub fn sort_by_key_fn<T, K, F>(items: &[T], key: F) -> Vec<T>
    where
        T: Clone,
        K: Ord,
        F: FnMut(&T) -> K,
    {
        let mut out = items.to_vec();
        out.sort_by_key(key);
        out
    }
    
    /// Sort strings alphabetically — the Rust parallel of
    /// `List.sort String.compare words`.
    ///
    /// `String::cmp` is lexicographic byte order; for ASCII this matches
    /// `String.compare`. For unicode-aware locale sorting, use the
    /// `unicode-collation` crate (out of scope here).
    pub fn sort_alphabetical(words: &[String]) -> Vec<String> {
        sort_by_comparator(words, |a, b| a.cmp(b))
    }
    
    /// Sort strings by length — the Rust parallel of
    /// `List.sort (fun a b -> compare (String.length a) (String.length b)) words`.
    ///
    /// Uses `sort_by_key` rather than `sort_by` because the comparator is a
    /// pure key extraction — the idiomatic Rust form.
    pub fn sort_by_length(words: &[String]) -> Vec<String> {
        sort_by_key_fn(words, |s| s.len())
    }
    
    /// Stable sort by length, then alphabetically as a tie-break. Demonstrates
    /// chained comparators — OCaml would write `compare (len a, a) (len b, b)`
    /// using the lexicographic tuple compare.
    pub fn sort_by_length_then_alpha(words: &[String]) -> Vec<String> {
        sort_by_comparator(words, |a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b)))
    }
    
    /// Generic descending sort — wraps any comparator and reverses its result.
    /// OCaml would write `fun a b -> compare b a`; Rust reverses the
    /// `Ordering` via `.reverse()`.
    pub fn sort_descending<T, F>(items: &[T], mut cmp: F) -> Vec<T>
    where
        T: Clone,
        F: FnMut(&T, &T) -> Ordering,
    {
        sort_by_comparator(items, move |a, b| cmp(a, b).reverse())
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        fn words() -> Vec<String> {
            ["banana", "apple", "cherry", "date"]
                .iter()
                .map(|s| (*s).to_string())
                .collect()
        }
    
        // --- alphabetical -------------------------------------------------
    
        #[test]
        fn test_alphabetical_matches_ocaml() {
            assert_eq!(
                sort_alphabetical(&words()),
                vec!["apple", "banana", "cherry", "date"]
            );
        }
    
        #[test]
        fn test_alphabetical_empty() {
            let empty: Vec<String> = Vec::new();
            assert_eq!(sort_alphabetical(&empty), Vec::<String>::new());
        }
    
        #[test]
        fn test_alphabetical_single() {
            let one = vec!["solo".to_string()];
            assert_eq!(sort_alphabetical(&one), vec!["solo"]);
        }
    
        #[test]
        fn test_alphabetical_does_not_mutate_input() {
            let input = words();
            let _sorted = sort_alphabetical(&input);
            // input retains original order — sort returned a fresh Vec
            assert_eq!(
                input,
                vec!["banana", "apple", "cherry", "date"]
                    .into_iter()
                    .map(str::to_string)
                    .collect::<Vec<_>>()
            );
        }
    
        // --- by length ----------------------------------------------------
    
        #[test]
        fn test_by_length_matches_ocaml() {
            // "date" (4) and "apple" (5) — "banana" and "cherry" both 6.
            let got = sort_by_length(&words());
            let lengths: Vec<usize> = got.iter().map(String::len).collect();
            assert_eq!(lengths, vec![4, 5, 6, 6]);
            // "date" must precede "apple" (strict length order)
            assert_eq!(got[0], "date");
            assert_eq!(got[1], "apple");
        }
    
        #[test]
        fn test_by_length_is_stable_for_ties() {
            // sort_by_key uses a stable sort — elements of equal length keep
            // their original relative order. "banana" appears before "cherry"
            // in the input, so it must stay before in the output.
            let got = sort_by_length(&words());
            assert_eq!(got[2], "banana");
            assert_eq!(got[3], "cherry");
        }
    
        // --- chained comparator -----------------------------------------
    
        #[test]
        fn test_length_then_alpha() {
            let got = sort_by_length_then_alpha(&words());
            // "banana" and "cherry" tie on length 6 — alpha tie-break puts
            // "banana" before "cherry" (same as stable sort in this case).
            assert_eq!(got, vec!["date", "apple", "banana", "cherry"]);
        }
    
        #[test]
        fn test_length_then_alpha_breaks_ties() {
            let input: Vec<String> = ["dog", "cat", "ant", "bee"]
                .iter()
                .map(|s| (*s).to_string())
                .collect();
            // all length 3 — falls through to alphabetical
            assert_eq!(
                sort_by_length_then_alpha(&input),
                vec!["ant", "bee", "cat", "dog"]
            );
        }
    
        // --- descending -------------------------------------------------
    
        #[test]
        fn test_descending_alphabetical() {
            let got = sort_descending(&words(), |a, b| a.cmp(b));
            assert_eq!(got, vec!["date", "cherry", "banana", "apple"]);
        }
    
        #[test]
        fn test_descending_by_length() {
            let got = sort_descending(&words(), |a, b| a.len().cmp(&b.len()));
            let lengths: Vec<usize> = got.iter().map(String::len).collect();
            assert_eq!(lengths, vec![6, 6, 5, 4]);
        }
    
        // --- generic sort_by_comparator on integers ---------------------
    
        #[test]
        fn test_generic_comparator_on_integers() {
            let xs = vec![3, 1, 4, 1, 5, 9, 2, 6];
            let got = sort_by_comparator(&xs, i32::cmp);
            assert_eq!(got, vec![1, 1, 2, 3, 4, 5, 6, 9]);
        }
    
        #[test]
        fn test_generic_key_fn_on_integers_abs() {
            let xs = vec![-3, 1, -4, 2, -5];
            let got = sort_by_key_fn(&xs, |x: &i32| x.abs());
            assert_eq!(got, vec![1, 2, -3, -4, -5]);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        fn words() -> Vec<String> {
            ["banana", "apple", "cherry", "date"]
                .iter()
                .map(|s| (*s).to_string())
                .collect()
        }
    
        // --- alphabetical -------------------------------------------------
    
        #[test]
        fn test_alphabetical_matches_ocaml() {
            assert_eq!(
                sort_alphabetical(&words()),
                vec!["apple", "banana", "cherry", "date"]
            );
        }
    
        #[test]
        fn test_alphabetical_empty() {
            let empty: Vec<String> = Vec::new();
            assert_eq!(sort_alphabetical(&empty), Vec::<String>::new());
        }
    
        #[test]
        fn test_alphabetical_single() {
            let one = vec!["solo".to_string()];
            assert_eq!(sort_alphabetical(&one), vec!["solo"]);
        }
    
        #[test]
        fn test_alphabetical_does_not_mutate_input() {
            let input = words();
            let _sorted = sort_alphabetical(&input);
            // input retains original order — sort returned a fresh Vec
            assert_eq!(
                input,
                vec!["banana", "apple", "cherry", "date"]
                    .into_iter()
                    .map(str::to_string)
                    .collect::<Vec<_>>()
            );
        }
    
        // --- by length ----------------------------------------------------
    
        #[test]
        fn test_by_length_matches_ocaml() {
            // "date" (4) and "apple" (5) — "banana" and "cherry" both 6.
            let got = sort_by_length(&words());
            let lengths: Vec<usize> = got.iter().map(String::len).collect();
            assert_eq!(lengths, vec![4, 5, 6, 6]);
            // "date" must precede "apple" (strict length order)
            assert_eq!(got[0], "date");
            assert_eq!(got[1], "apple");
        }
    
        #[test]
        fn test_by_length_is_stable_for_ties() {
            // sort_by_key uses a stable sort — elements of equal length keep
            // their original relative order. "banana" appears before "cherry"
            // in the input, so it must stay before in the output.
            let got = sort_by_length(&words());
            assert_eq!(got[2], "banana");
            assert_eq!(got[3], "cherry");
        }
    
        // --- chained comparator -----------------------------------------
    
        #[test]
        fn test_length_then_alpha() {
            let got = sort_by_length_then_alpha(&words());
            // "banana" and "cherry" tie on length 6 — alpha tie-break puts
            // "banana" before "cherry" (same as stable sort in this case).
            assert_eq!(got, vec!["date", "apple", "banana", "cherry"]);
        }
    
        #[test]
        fn test_length_then_alpha_breaks_ties() {
            let input: Vec<String> = ["dog", "cat", "ant", "bee"]
                .iter()
                .map(|s| (*s).to_string())
                .collect();
            // all length 3 — falls through to alphabetical
            assert_eq!(
                sort_by_length_then_alpha(&input),
                vec!["ant", "bee", "cat", "dog"]
            );
        }
    
        // --- descending -------------------------------------------------
    
        #[test]
        fn test_descending_alphabetical() {
            let got = sort_descending(&words(), |a, b| a.cmp(b));
            assert_eq!(got, vec!["date", "cherry", "banana", "apple"]);
        }
    
        #[test]
        fn test_descending_by_length() {
            let got = sort_descending(&words(), |a, b| a.len().cmp(&b.len()));
            let lengths: Vec<usize> = got.iter().map(String::len).collect();
            assert_eq!(lengths, vec![6, 6, 5, 4]);
        }
    
        // --- generic sort_by_comparator on integers ---------------------
    
        #[test]
        fn test_generic_comparator_on_integers() {
            let xs = vec![3, 1, 4, 1, 5, 9, 2, 6];
            let got = sort_by_comparator(&xs, i32::cmp);
            assert_eq!(got, vec![1, 1, 2, 3, 4, 5, 6, 9]);
        }
    
        #[test]
        fn test_generic_key_fn_on_integers_abs() {
            let xs = vec![-3, 1, -4, 2, -5];
            let got = sort_by_key_fn(&xs, |x: &i32| x.abs());
            assert_eq!(got, vec![1, 2, -3, -4, -5]);
        }
    }

    Deep Comparison

    OCaml vs Rust: List Sort (with custom comparator)

    Side-by-Side Code

    OCaml

    let words = ["banana"; "apple"; "cherry"; "date"]
    
    (* Alphabetical — String.compare is already a three-way comparator *)
    let by_alphabet = List.sort String.compare words
    
    (* Custom comparator: compare by string length *)
    let by_length =
      List.sort (fun a b -> compare (String.length a) (String.length b)) words
    
    (* Descending — reverse the arguments to the comparator *)
    let by_alpha_desc = List.sort (fun a b -> String.compare b a) words
    

    Rust (idiomatic)

    use std::cmp::Ordering;
    
    pub fn sort_by_comparator<T, F>(items: &[T], cmp: F) -> Vec<T>
    where
        T: Clone,
        F: FnMut(&T, &T) -> Ordering,
    {
        let mut out = items.to_vec();
        out.sort_by(cmp);
        out
    }
    
    pub fn sort_alphabetical(words: &[String]) -> Vec<String> {
        sort_by_comparator(words, |a, b| a.cmp(b))
    }
    

    Rust (functional / key-based)

    pub fn sort_by_key_fn<T, K, F>(items: &[T], key: F) -> Vec<T>
    where
        T: Clone,
        K: Ord,
        F: FnMut(&T) -> K,
    {
        let mut out = items.to_vec();
        out.sort_by_key(key);
        out
    }
    
    pub fn sort_by_length(words: &[String]) -> Vec<String> {
        sort_by_key_fn(words, |s| s.len())
    }
    

    Type Signatures

    ConceptOCamlRust
    List.sort signatureval sort : ('a -> 'a -> int) -> 'a list -> 'a listfn sort_by<F: FnMut(&T, &T) -> Ordering>(&mut [T], F)
    Comparator resultint (sign: <0, 0, >0)std::cmp::Ordering (Less / Equal / Greater)
    Key-based shortcut— (inline in comparator)fn sort_by_key<K: Ord, F: FnMut(&T) -> K>(&mut [T], F)
    Immutable flavourbuilt-in — returns a new listitems.to_vec() then .sort_by(...) — one clone per call
    Descending orderfun a b -> cmp b a|a, b| cmp(a, b).reverse()

    Key Insights

  • **List.sortslice::sort_by** — same O(n log n) stable merge-sort (OCaml stdlib) / Timsort-style pattern-defeating sort (Rust stdlib). The shapes are identical apart from the comparator return type.
  • **int comparator vs Ordering enum** — OCaml uses compare returning an int whose sign matters. Rust's Ordering is a three-variant enum that forbids the classic bug of a - b overflowing for large i32 values. a.cmp(&b) is the safe equivalent.
  • In-place by default — Rust's slice methods mutate, so "returning a new list" is opt-in: let mut v = items.to_vec(); v.sort_by(...); v. OCaml's List.sort is inherently pure because the list cells are immutable.
  • **sort_by_key has no OCaml analogue** — in OCaml you re-extract the key inside every comparator call. Rust exposes sort_by_key (simple) and sort_by_cached_key (caches expensive keys), saving redundant work.
  • Stability matters, and is named — both stdlibs default to stable. Rust is explicit: sort_by_unstable_by is a separate method you pick for speed. OCaml only ships the stable variant.
  • When to Use Each Style

    **Use idiomatic Rust (sort_by_key) when:** the comparator is "compare some extracted value" — by length, by a struct field, by key.to_lowercase(). It is both shorter and faster because each key is computed once.

    **Use sort_by when:** the comparator genuinely needs both elements — multi-level sorts (primary.then_with(|| secondary)), or domain-specific orderings that cannot be reduced to a single Ord key.

    Open Source Repos