List.sort — Sort with Custom Comparator
Tutorial
The Problem
Sort a list of strings in three different ways: lexicographically, by string length (with ties broken alphabetically), and in descending order. The core challenge is implementing a generic sort_with function that accepts a custom comparator, directly mirroring the signature and behavior of OCaml's List.sort. This example demonstrates how first-class comparison functions enable flexible, reusable sorting without duplicating traversal logic.
🎯 Learning Outcomes
List.sort cmp xs maps to Rust's slice::sort_by(|a, b| cmp(a, b)) and why the comparator return type differsFn(&T, &T) -> Ordering) Rust requires.then() on Ordering — equivalent to the OCaml pattern compare (key a) (key b)List.sort and Rust's sort_by guarantee stability, and what that means for tie-breakingCode Example
pub fn sort_strings<'a>(words: &[&'a str]) -> Vec<&'a str> {
let mut result = words.to_vec();
result.sort();
result
}Key Differences
int (negative/zero/positive), following the C convention; Rust uses the std::cmp::Ordering enum — same three-way semantics expressed as a typed value rather than a numeric convention.List.sort returns a new list because lists are immutable; Rust's sort_by sorts the slice in place, requiring an explicit .to_vec() clone to achieve value-returning behavior.if compare_first = 0 then compare_second else compare_first; Rust provides .then() and .then_with() on Ordering, making multi-key comparisons concise and branch-free.Fn(&T, &T) -> std::cmp::Ordering, which the compiler enforces at the call site.OCaml Approach
OCaml's List.sort takes a comparison function returning a negative integer, zero, or a positive integer — the same convention as C's qsort. For lexicographic order, String.compare is passed directly as a first-class function value. For length-based ordering, an anonymous function fun a b -> compare (String.length a) (String.length b) extracts the key before delegating to the polymorphic compare. Descending order simply reverses the argument order: fun a b -> String.compare b a. OCaml lists are immutable, so List.sort always returns a new list; no mutation is observable at the call site.
Full Source
#![allow(dead_code)]
//! List.sort — Sort with Custom Comparator
//! See example.ml for OCaml reference
//!
//! OCaml's `List.sort cmp xs` sorts using a comparison function that returns negative/zero/positive.
//! Rust's `slice::sort_by` takes an `Ordering`-returning comparator.
/// Sort a slice of strings lexicographically (ascending).
/// Mirrors OCaml: `List.sort String.compare words`
pub fn sort_strings<'a>(words: &[&'a str]) -> Vec<&'a str> {
let mut result = words.to_vec();
result.sort();
result
}
/// Sort by string length (ascending), ties broken by lexicographic order.
/// Mirrors OCaml: `List.sort (fun a b -> compare (String.length a) (String.length b)) words`
pub fn sort_by_length<'a>(words: &[&'a str]) -> Vec<&'a str> {
let mut result = words.to_vec();
result.sort_by(|a, b| a.len().cmp(&b.len()).then(a.cmp(b)));
result
}
/// Sort in descending lexicographic order.
/// Mirrors OCaml: `List.sort (fun a b -> String.compare b a) words`
pub fn sort_descending<'a>(words: &[&'a str]) -> Vec<&'a str> {
let mut result = words.to_vec();
result.sort_by(|a, b| b.cmp(a));
result
}
/// Generic sort with a custom comparator — mirrors OCaml's `List.sort f xs`.
pub fn sort_with<T: Clone, F>(items: &[T], compare: F) -> Vec<T>
where
F: Fn(&T, &T) -> std::cmp::Ordering,
{
let mut result = items.to_vec();
result.sort_by(compare);
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sort_strings() {
let words = vec!["banana", "apple", "cherry", "date"];
assert_eq!(sort_strings(&words), vec!["apple", "banana", "cherry", "date"]);
}
#[test]
fn test_sort_by_length() {
let words = vec!["banana", "apple", "cherry", "date"];
// "date"(4), "apple"(5), "banana"(6), "cherry"(6) — ties broken alphabetically
assert_eq!(sort_by_length(&words), vec!["date", "apple", "banana", "cherry"]);
}
#[test]
fn test_sort_descending() {
let words = vec!["banana", "apple", "cherry", "date"];
assert_eq!(sort_descending(&words), vec!["date", "cherry", "banana", "apple"]);
}
#[test]
fn test_sort_empty() {
let empty: Vec<&str> = vec![];
assert_eq!(sort_strings(&empty), Vec::<&str>::new());
}
#[test]
fn test_sort_with_custom() {
let nums = vec![3i32, 1, 4, 1, 5, 9, 2, 6];
let sorted = sort_with(&nums, |a, b| a.cmp(b));
assert_eq!(sorted, vec![1, 1, 2, 3, 4, 5, 6, 9]);
}
#[test]
fn test_sort_with_reverse() {
let nums = vec![3i32, 1, 4, 1, 5];
let sorted = sort_with(&nums, |a, b| b.cmp(a));
assert_eq!(sorted, vec![5, 4, 3, 1, 1]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sort_strings() {
let words = vec!["banana", "apple", "cherry", "date"];
assert_eq!(sort_strings(&words), vec!["apple", "banana", "cherry", "date"]);
}
#[test]
fn test_sort_by_length() {
let words = vec!["banana", "apple", "cherry", "date"];
// "date"(4), "apple"(5), "banana"(6), "cherry"(6) — ties broken alphabetically
assert_eq!(sort_by_length(&words), vec!["date", "apple", "banana", "cherry"]);
}
#[test]
fn test_sort_descending() {
let words = vec!["banana", "apple", "cherry", "date"];
assert_eq!(sort_descending(&words), vec!["date", "cherry", "banana", "apple"]);
}
#[test]
fn test_sort_empty() {
let empty: Vec<&str> = vec![];
assert_eq!(sort_strings(&empty), Vec::<&str>::new());
}
#[test]
fn test_sort_with_custom() {
let nums = vec![3i32, 1, 4, 1, 5, 9, 2, 6];
let sorted = sort_with(&nums, |a, b| a.cmp(b));
assert_eq!(sorted, vec![1, 1, 2, 3, 4, 5, 6, 9]);
}
#[test]
fn test_sort_with_reverse() {
let nums = vec![3i32, 1, 4, 1, 5];
let sorted = sort_with(&nums, |a, b| b.cmp(a));
assert_eq!(sorted, vec![5, 4, 3, 1, 1]);
}
}
Deep Comparison
OCaml vs Rust: List.sort — Sort with Custom Comparator
Side-by-Side Code
OCaml
let words = ["banana"; "apple"; "cherry"; "date"]
(* Idiomatic: sort lexicographically using String.compare *)
let sorted = List.sort String.compare words
(* Sort by string length, ties broken lexicographically *)
let by_length = List.sort (fun a b ->
compare (String.length a) (String.length b)) words
(* Sort in descending order by reversing the comparator *)
let descending = List.sort (fun a b -> String.compare b a) words
Rust (idiomatic)
pub fn sort_strings<'a>(words: &[&'a str]) -> Vec<&'a str> {
let mut result = words.to_vec();
result.sort();
result
}
Rust (functional — sort_by with custom comparator)
pub fn sort_by_length<'a>(words: &[&'a str]) -> Vec<&'a str> {
let mut result = words.to_vec();
result.sort_by(|a, b| a.len().cmp(&b.len()).then(a.cmp(b)));
result
}
pub fn sort_descending<'a>(words: &[&'a str]) -> Vec<&'a str> {
let mut result = words.to_vec();
result.sort_by(|a, b| b.cmp(a));
result
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Basic sort | val sort : ('a -> 'a -> int) -> 'a list -> 'a list | fn sort_strings<'a>(words: &[&'a str]) -> Vec<&'a str> |
| Comparator type | 'a -> 'a -> int (negative/zero/positive) | FnMut(&T, &T) -> Ordering |
| Return type | New 'a list (persistent) | Vec<T> (new owned vector) |
| Custom comparator | List.sort (fun a b -> ...) xs | slice.sort_by(\|a, b\| ...) |
| Stable sort | List.stable_sort cmp xs | slice.sort_by(...) (stable by default) |
Key Insights
int (negative/zero/positive); Rust comparators return std::cmp::Ordering (Less/Equal/Greater). The Ordering type is more explicit and eliminates off-by-one bugs from integer comparisons.List.sort returns a new list (lists are immutable linked structures). Rust's sort/sort_by mutates a Vec in place — you clone first if you need the original.sort/sort_by is guaranteed stable (merge sort). OCaml's List.sort is also stable; List.fast_sort may not be.Ordering::then / then_with chains multiple sort keys cleanly — equivalent to OCaml's nested compare calls but reads left-to-right without nesting.&[&str] (a borrowed slice of borrowed strings) and return Vec<&str> (owned vector of the same borrowed strings). No string data is copied — only the pointers.When to Use Each Style
**Use sort() when:** sorting types that implement Ord naturally — strings, integers, tuples. Zero boilerplate, same as OCaml's List.sort String.compare.
**Use sort_by(\|a, b\| ...) when:** you need a custom comparator — sorting by a field, reversing order, or chaining multiple criteria. Equivalent to OCaml's List.sort (fun a b -> ...).
**Use sort_by_key(\|x\| ...) when:** sorting by a single extracted key — cleaner than sort_by for simple cases like sort_by_key(\|s\| s.len()).
Exercises
sort_by_key that takes a key extraction function f: Fn(&T) -> K where K: Ord, mirroring OCaml's common idiom List.sort (fun a b -> compare (f a) (f b)) xs. Verify it produces the same output as sort_with with an equivalent comparator.(String, i32) pairs: first by the integer descending, then by string ascending for ties. Express the comparator using .then() chaining rather than an if expression.is_sorted_by that takes a slice and a comparator and returns bool — true if every adjacent pair satisfies cmp(a, b) != Ordering::Greater. Write tests covering empty, single-element, ascending, and descending inputs.