ExamplesBy LevelBy TopicLearning Paths
1200 Fundamental

List Filter — Select Elements by Predicate

ListsHOF

Tutorial

The Problem

Given a list of integers, keep only those elements satisfying a boolean predicate. Demonstrates List.filter as a higher-order function that decouples the iteration strategy from the selection logic.

🎯 Learning Outcomes

  • • How OCaml's List.filter maps to Rust's Iterator::filter combinator
  • • The difference between returning borrowed refs (Vec<&T>) vs owned values (Vec<T>) in Rust
  • • How to write a recursive Rust filter without hitting infinite type instantiation (use an inner fn go with &dyn Fn)
  • • Closures as first-class predicates in both languages
  • 🦀 The Rust Way

    Rust's iterator chain list.iter().filter(|x| predicate(x)).collect() mirrors OCaml semantically. The key decision is whether to return Vec<&T> (borrowed, zero-copy) or Vec<T> (owned, requires Clone). The idiomatic choice depends on the caller's ownership needs. For the recursive variant, a nested fn go with &dyn Fn avoids the infinite monomorphization that arises from passing &predicate recursively with a generic F.

    Code Example

    pub fn filter<T, F>(list: &[T], predicate: F) -> Vec<&T>
    where
        F: Fn(&T) -> bool,
    {
        list.iter().filter(|x| predicate(x)).collect()
    }

    Key Differences

  • Allocation model: OCaml creates a new linked list; Rust collects into a Vec (contiguous memory).
  • Ownership: Rust distinguishes Vec<&T> (borrowed refs) from Vec<T> (owned copies) — OCaml has no such split.
  • Recursion helper: Rust's monomorphic generics require an inner fn go(&dyn Fn) to recurse without type explosion; OCaml recurses freely.
  • Predicate type: OCaml uses 'a -> bool; Rust uses Fn(&T) -> bool, explicitly borrowed.
  • OCaml Approach

    OCaml's List.filter pred lst is a standard library function implemented by structural recursion. The predicate is a plain closure fun x -> x mod 2 = 0. The result is a new list — OCaml lists are immutable and singly-linked, so this allocates fresh cons cells.

    Full Source

    #![allow(dead_code)]
    
    /// Idiomatic Rust: filter a slice using a predicate, returning borrowed refs.
    /// Takes &[T] to borrow without allocation; predicate is a closure or fn.
    pub fn filter<T, F>(list: &[T], predicate: F) -> Vec<&T>
    where
        F: Fn(&T) -> bool,
    {
        list.iter().filter(|x| predicate(x)).collect()
    }
    
    /// Filter and clone elements (owned output), mirroring OCaml's List.filter.
    pub fn filter_cloned<T: Clone, F>(list: &[T], predicate: F) -> Vec<T>
    where
        F: Fn(&T) -> bool,
    {
        list.iter().filter(|x| predicate(x)).cloned().collect()
    }
    
    /// Functional/recursive — mirrors the OCaml recursive pattern explicitly.
    /// Uses an inner fn with `&dyn Fn` to avoid infinite type instantiation.
    pub fn filter_recursive<T: Clone, F>(list: &[T], predicate: F) -> Vec<T>
    where
        F: Fn(&T) -> bool,
    {
        fn go<T: Clone>(list: &[T], predicate: &dyn Fn(&T) -> bool) -> Vec<T> {
            match list {
                [] => vec![],
                [head, tail @ ..] => {
                    let mut rest = go(tail, predicate);
                    if predicate(head) {
                        let mut result = vec![head.clone()];
                        result.append(&mut rest);
                        result
                    } else {
                        rest
                    }
                }
            }
        }
        go(list, &predicate)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_filter_empty() {
            let empty: &[i32] = &[];
            assert_eq!(filter(empty, |x| x % 2 == 0), Vec::<&i32>::new());
        }
    
        #[test]
        fn test_filter_evens() {
            let numbers = [1, 2, 3, 4, 5, 6, 7, 8];
            let evens: Vec<&i32> = filter(&numbers, |x| x % 2 == 0);
            assert_eq!(evens, [&2, &4, &6, &8]);
        }
    
        #[test]
        fn test_filter_odds() {
            let numbers = [1, 2, 3, 4, 5, 6, 7, 8];
            let odds: Vec<&i32> = filter(&numbers, |x| x % 2 != 0);
            assert_eq!(odds, [&1, &3, &5, &7]);
        }
    
        #[test]
        fn test_filter_none_match() {
            let numbers = [1, 3, 5, 7];
            assert_eq!(filter(&numbers, |x| x % 2 == 0), Vec::<&i32>::new());
        }
    
        #[test]
        fn test_filter_all_match() {
            let numbers = [2, 4, 6];
            let result = filter(&numbers, |x| x % 2 == 0);
            assert_eq!(result, [&2, &4, &6]);
        }
    
        #[test]
        fn test_filter_cloned_evens() {
            let numbers = [1, 2, 3, 4, 5, 6, 7, 8];
            let evens = filter_cloned(&numbers, |x| x % 2 == 0);
            assert_eq!(evens, [2, 4, 6, 8]);
        }
    
        #[test]
        fn test_filter_recursive_evens() {
            let numbers = [1, 2, 3, 4, 5, 6, 7, 8];
            let evens = filter_recursive(&numbers, |x| x % 2 == 0);
            assert_eq!(evens, [2, 4, 6, 8]);
        }
    
        #[test]
        fn test_filter_recursive_empty() {
            let empty: &[i32] = &[];
            assert_eq!(filter_recursive(empty, |x| x % 2 == 0), Vec::<i32>::new());
        }
    
        #[test]
        fn test_filter_strings() {
            let words = ["apple", "banana", "cherry", "date"];
            let long_words: Vec<&&str> = filter(&words, |w| w.len() > 5);
            assert_eq!(long_words, [&"banana", &"cherry"]);
        }
    
        #[test]
        fn test_filter_single_element_match() {
            let numbers = [42];
            assert_eq!(filter(&numbers, |x| *x == 42), [&42]);
        }
    
        #[test]
        fn test_filter_single_element_no_match() {
            let numbers = [42];
            assert_eq!(filter(&numbers, |x| *x == 0), Vec::<&i32>::new());
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_filter_empty() {
            let empty: &[i32] = &[];
            assert_eq!(filter(empty, |x| x % 2 == 0), Vec::<&i32>::new());
        }
    
        #[test]
        fn test_filter_evens() {
            let numbers = [1, 2, 3, 4, 5, 6, 7, 8];
            let evens: Vec<&i32> = filter(&numbers, |x| x % 2 == 0);
            assert_eq!(evens, [&2, &4, &6, &8]);
        }
    
        #[test]
        fn test_filter_odds() {
            let numbers = [1, 2, 3, 4, 5, 6, 7, 8];
            let odds: Vec<&i32> = filter(&numbers, |x| x % 2 != 0);
            assert_eq!(odds, [&1, &3, &5, &7]);
        }
    
        #[test]
        fn test_filter_none_match() {
            let numbers = [1, 3, 5, 7];
            assert_eq!(filter(&numbers, |x| x % 2 == 0), Vec::<&i32>::new());
        }
    
        #[test]
        fn test_filter_all_match() {
            let numbers = [2, 4, 6];
            let result = filter(&numbers, |x| x % 2 == 0);
            assert_eq!(result, [&2, &4, &6]);
        }
    
        #[test]
        fn test_filter_cloned_evens() {
            let numbers = [1, 2, 3, 4, 5, 6, 7, 8];
            let evens = filter_cloned(&numbers, |x| x % 2 == 0);
            assert_eq!(evens, [2, 4, 6, 8]);
        }
    
        #[test]
        fn test_filter_recursive_evens() {
            let numbers = [1, 2, 3, 4, 5, 6, 7, 8];
            let evens = filter_recursive(&numbers, |x| x % 2 == 0);
            assert_eq!(evens, [2, 4, 6, 8]);
        }
    
        #[test]
        fn test_filter_recursive_empty() {
            let empty: &[i32] = &[];
            assert_eq!(filter_recursive(empty, |x| x % 2 == 0), Vec::<i32>::new());
        }
    
        #[test]
        fn test_filter_strings() {
            let words = ["apple", "banana", "cherry", "date"];
            let long_words: Vec<&&str> = filter(&words, |w| w.len() > 5);
            assert_eq!(long_words, [&"banana", &"cherry"]);
        }
    
        #[test]
        fn test_filter_single_element_match() {
            let numbers = [42];
            assert_eq!(filter(&numbers, |x| *x == 42), [&42]);
        }
    
        #[test]
        fn test_filter_single_element_no_match() {
            let numbers = [42];
            assert_eq!(filter(&numbers, |x| *x == 0), Vec::<&i32>::new());
        }
    }

    Deep Comparison

    OCaml vs Rust: List Filter — Select Elements by Predicate

    Side-by-Side Code

    OCaml

    let numbers = [1; 2; 3; 4; 5; 6; 7; 8]
    let evens = List.filter (fun x -> x mod 2 = 0) numbers
    let odds  = List.filter (fun x -> x mod 2 <> 0) numbers
    

    Rust (idiomatic — borrowed refs)

    pub fn filter<T, F>(list: &[T], predicate: F) -> Vec<&T>
    where
        F: Fn(&T) -> bool,
    {
        list.iter().filter(|x| predicate(x)).collect()
    }
    

    Rust (idiomatic — owned, mirrors OCaml output)

    pub fn filter_cloned<T: Clone, F>(list: &[T], predicate: F) -> Vec<T>
    where
        F: Fn(&T) -> bool,
    {
        list.iter().filter(|x| predicate(x)).cloned().collect()
    }
    

    Rust (functional/recursive)

    pub fn filter_recursive<T: Clone, F>(list: &[T], predicate: F) -> Vec<T>
    where
        F: Fn(&T) -> bool,
    {
        fn go<T: Clone>(list: &[T], predicate: &dyn Fn(&T) -> bool) -> Vec<T> {
            match list {
                [] => vec![],
                [head, tail @ ..] => {
                    let mut rest = go(tail, predicate);
                    if predicate(head) {
                        let mut result = vec![head.clone()];
                        result.append(&mut rest);
                        result
                    } else {
                        rest
                    }
                }
            }
        }
        go(list, &predicate)
    }
    

    OCaml (recursive)

    let rec filter_rec pred = function
      | [] -> []
      | x :: rest ->
        if pred x then x :: filter_rec pred rest
        else filter_rec pred rest
    

    Type Signatures

    ConceptOCamlRust
    Filter functionval filter : ('a -> bool) -> 'a list -> 'a listfn filter<T, F>(list: &[T], predicate: F) -> Vec<&T>
    List type'a list&[T] (slice) or Vec<T>
    Predicate type'a -> boolFn(&T) -> bool
    Owned outputimplicit (all values)requires T: Clone, use .cloned()

    Key Insights

  • **iter().filter().collect() is the idiomatic translation of List.filter** — the combinator chain mirrors OCaml's functional style directly without a visible loop.
  • Borrowed vs owned output is a Rust-specific decision — OCaml's List.filter always returns a new list of the same type. In Rust you choose: Vec<&T> avoids allocation but ties lifetimes to the input slice; Vec<T> requires Clone but is independent. Neither is "wrong" — the right choice depends on how the caller uses the result.
  • **Recursive Rust requires a &dyn Fn helper to avoid infinite monomorphization** — if you write filter_recursive(tail, &predicate) where predicate: F is generic, each recursive call instantiates F = &F = &&F = ... and the compiler hits its recursion limit. The fix is a non-generic inner function fn go(..., predicate: &dyn Fn(...)) that uses dynamic dispatch for the predicate.
  • **Slice patterns ([head, tail @ ..]) mirror OCaml cons patterns** — OCaml's x :: rest destructures a cons cell; Rust's [head, tail @ ..] destructures a slice. Both express "take the head, recurse on the tail" without indexing.
  • Performance: iterator chains are lazy, recursive is eager — Rust's filter().collect() builds the Vec in a single pass through the iterator. The recursive version builds it in reverse (appending to the front via vec![head]; result.append(rest)) which is O(n²) due to Vec front-insertion. For production code, always prefer the iterator version.
  • When to Use Each Style

    Use idiomatic iterator Rust when: filtering any slice or collection in production code — it's O(n), composable, and the most readable approach.

    Use recursive Rust when: demonstrating the structural correspondence to OCaml, teaching pattern matching on slices, or when the problem is inherently recursive and filter is just a component.

    Open Source Repos