๐Ÿฆ€ Functional Rust

055: List Filter from Scratch

Difficulty: 1 Level: Beginner Implement `filter` from first principles using a predicate function โ€” and derive it from `fold`.

The Problem This Solves

You have a list of orders and you want only the pending ones. A list of log lines and you want only the errors. A list of numbers and you want only the evens. Without `filter`, you write the same accumulator loop for every predicate, sprinkling `if` guards through your business logic. `filter` separates the what to keep (the predicate) from the how to keep it (the traversal). You write `filter(orders, |o| o.status == Pending)` and the loop disappears. Like `map`, `filter` is derivable from `fold` โ€” which reveals that fold truly is the universal list combinator.

The Intuition

`filter` is a sieve. You shake the list; elements that pass the predicate fall through, the rest are discarded. The order of survivors is preserved โ€” an important guarantee for correctness. The predicate is just a function that returns `bool`. Any function. `filter` doesn't care what the test is; it just applies it to each element and decides keep or discard.

How It Works in Rust

// Idiomatic: iterator adapter
pub fn filter<T: Clone, P: Fn(&T) -> bool>(list: &[T], p: P) -> Vec<T> {
 list.iter().filter(|x| p(x)).cloned().collect()
 // .cloned() converts &T references back to owned T values
}

// Recursive: mirrors OCaml's pattern match
pub fn filter_rec<T: Clone, P: Fn(&T) -> bool>(list: &[T], p: P) -> Vec<T> {
 fn go<T: Clone>(list: &[T], p: &dyn Fn(&T) -> bool) -> Vec<T> {
     match list {
         [] => vec![],
         [head, tail @ ..] => {
             let rest = go(tail, p);
             if p(head) {
                 // prepend head to rest โ€” preserves order
                 let mut result = vec![head.clone()];
                 result.extend(rest);
                 result
             } else {
                 rest  // skip head
             }
         }
     }
 }
 go(list, &p)
 // Note: &dyn Fn breaks the infinite type recursion that &p would cause
}

// Fold: filter as fold
pub fn filter_fold<T: Clone, P: Fn(&T) -> bool>(list: &[T], p: P) -> Vec<T> {
 list.iter().fold(Vec::new(), |mut acc, x| {
     if p(x) { acc.push(x.clone()); }
     acc
 })
}
The `&dyn Fn` in the recursive version breaks an otherwise infinite type: passing `&p` in a generic recursive function would create `&&p`, `&&&p`, โ€ฆ infinitely. Dynamic dispatch via `&dyn Fn` stops the monomorphization chain.

What This Unlocks

Key Differences

ConceptOCamlRust
Predicate type`'a -> bool``Fn(&T) -> bool`
Partial application`let evens = filter (fun n -> n mod 2 = 0)`Closure: `\nums\filter(nums, \n\n % 2 == 0)`
Recursive genericPolymorphic recursion straightforwardNeeds `&dyn Fn` break-point
Order guaranteeLeft-to-right, guaranteedSame โ€” both preserve relative order
`cloned()`Not needed (GC copies)Required to get `T` from `&T` iterator
//! # List Filter from Scratch
//! CS3110 โ€” Deriving `List.filter`: predicate functions and preserving order.

/// Iterator-based filter โ€” idiomatic Rust, mirrors OCaml's `List.filter`.
///
/// Uses `Iterator::filter` to select elements satisfying predicate `p`.
/// Preserves order. Zero-copy predicate โ€” `p` borrows each element.
pub fn filter<T, P>(list: &[T], p: P) -> Vec<T>
where
    T: Clone,
    P: Fn(&T) -> bool,
{
    list.iter().filter(|x| p(x)).cloned().collect()
}

/// Recursive filter โ€” structural translation of the OCaml pattern-match.
///
/// ```text
/// let rec filter p = function
///   | [] -> []
///   | h :: t -> if p h then h :: filter p t else filter p t
/// ```
///
/// Uses a `&dyn Fn` inner helper to avoid monomorphization explosion from
/// passing `&p` through recursive calls (which would wrap the type infinitely).
pub fn filter_rec<T, P>(list: &[T], p: P) -> Vec<T>
where
    T: Clone,
    P: Fn(&T) -> bool,
{
    fn go<T: Clone>(list: &[T], p: &dyn Fn(&T) -> bool) -> Vec<T> {
        match list {
            [] => vec![],
            [head, tail @ ..] => {
                let mut rest = go(tail, p);
                if p(head) {
                    let mut result = vec![head.clone()];
                    result.append(&mut rest);
                    result
                } else {
                    rest
                }
            }
        }
    }
    go(list, &p)
}

/// Fold-based filter โ€” builds result via `fold`, applying predicate in accumulator.
pub fn filter_fold<T, P>(list: &[T], p: P) -> Vec<T>
where
    T: Clone,
    P: Fn(&T) -> bool,
{
    list.iter().fold(Vec::new(), |mut acc, x| {
        if p(x) {
            acc.push(x.clone());
        }
        acc
    })
}

/// Convenience: keep even integers.
pub fn evens(nums: &[i32]) -> Vec<i32> {
    filter(nums, |n| n % 2 == 0)
}

/// Convenience: keep odd integers.
pub fn odds(nums: &[i32]) -> Vec<i32> {
    filter(nums, |n| n % 2 != 0)
}

/// Convenience: keep positive integers.
pub fn pos(nums: &[i32]) -> Vec<i32> {
    filter(nums, |n| *n > 0)
}

#[cfg(test)]
mod tests {
    use super::*;

    const NUMS: &[i32] = &[-3, -1, 0, 2, 4, 5, 7];

    // --- evens / odds / pos (matches OCaml demo output) ---

    #[test]
    fn test_evens() {
        assert_eq!(evens(NUMS), vec![0, 2, 4]);
    }

    #[test]
    fn test_odds() {
        assert_eq!(odds(NUMS), vec![-3, -1, 5, 7]);
    }

    #[test]
    fn test_pos() {
        assert_eq!(pos(NUMS), vec![2, 4, 5, 7]);
    }

    // --- all three implementations agree ---

    #[test]
    fn test_all_impls_agree() {
        let is_even = |n: &i32| n % 2 == 0;
        let a = filter(NUMS, is_even);
        let b = filter_rec(NUMS, is_even);
        let c = filter_fold(NUMS, is_even);
        assert_eq!(a, b);
        assert_eq!(b, c);
    }

    // --- edge cases ---

    #[test]
    fn test_empty() {
        let empty: &[i32] = &[];
        assert_eq!(filter(empty, |_| true), Vec::<i32>::new());
        assert_eq!(filter_rec(empty, |_| true), Vec::<i32>::new());
        assert_eq!(filter_fold(empty, |_| true), Vec::<i32>::new());
    }

    #[test]
    fn test_none_match() {
        assert_eq!(filter(&[1, 3, 5], |n| n % 2 == 0), Vec::<i32>::new());
        assert_eq!(filter_rec(&[1, 3, 5], |n| n % 2 == 0), Vec::<i32>::new());
        assert_eq!(filter_fold(&[1, 3, 5], |n| n % 2 == 0), Vec::<i32>::new());
    }

    #[test]
    fn test_all_match() {
        assert_eq!(filter(&[2, 4, 6], |n| n % 2 == 0), vec![2, 4, 6]);
    }

    #[test]
    fn test_order_preserved() {
        // Filter must keep relative order of surviving elements.
        let input = &[5, 1, 4, 2, 3];
        let expected = vec![4, 2];
        assert_eq!(filter(input, |n| n % 2 == 0), expected);
        assert_eq!(filter_rec(input, |n| n % 2 == 0), expected);
        assert_eq!(filter_fold(input, |n| n % 2 == 0), expected);
    }

    #[test]
    fn test_generic_strings() {
        let words = vec!["hello", "hi", "world", "hey"];
        let h_words: Vec<&str> = filter(&words, |w| w.starts_with('h'));
        assert_eq!(h_words, vec!["hello", "hi", "hey"]);
    }
}

fn main() {
    println!("{:?}", evens(NUMS), vec![0, 2, 4]);
    println!("{:?}", odds(NUMS), vec![-3, -1, 5, 7]);
    println!("{:?}", pos(NUMS), vec![2, 4, 5, 7]);
}
let rec filter p = function
  | [] -> []
  | h :: t -> if p h then h :: filter p t else filter p t

let evens = filter (fun n -> n mod 2 = 0)
let odds = filter (fun n -> n mod 2 <> 0)
let pos = filter (fun n -> n > 0)

let () =
  let nums = [-3; -1; 0; 2; 4; 5; 7] in
  List.iter (Printf.printf "%d ") (evens nums); print_newline ();
  List.iter (Printf.printf "%d ") (odds nums); print_newline ();
  List.iter (Printf.printf "%d ") (pos nums); print_newline ()

๐Ÿ“Š Detailed Comparison

Comparison: List Filter from Scratch

OCaml โ€” recursive pattern match

๐Ÿช Show OCaml equivalent
let rec filter p = function
| [] -> []
| h :: t -> if p h then h :: filter p t else filter p t

Rust โ€” iterator (idiomatic)

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

Rust โ€” recursive (structural translation)

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

Rust โ€” fold

pub fn filter_fold<T: Clone, P: Fn(&T) -> bool>(list: &[T], p: P) -> Vec<T> {
 list.iter().fold(Vec::new(), |mut acc, x| {
     if p(x) { acc.push(x.clone()); }
     acc
 })
}

Comparison Table

AspectOCamlRust
Base case`\[] -> []``[] => vec![]`
Recursive case`\h :: t -> ...``[head, tail @ ..] => ...`
Predicate type`'a -> bool``Fn(&T) -> bool`
Partial application`let evens = filter f`closure wrapping
Recursion safetystructural, GC-managedneeds `&dyn Fn` to avoid monomorphization loop
Iterator style`List.filter` in stdlib`Iterator::filter` in stdlib

Type Signatures

  • OCaml: `val filter : ('a -> bool) -> 'a list -> 'a list`
  • Rust: `fn filter<T: Clone, P: Fn(&T) -> bool>(list: &[T], p: P) -> Vec<T>`

Takeaways

1. Rust's `[head, tail @ ..]` slice pattern is the structural analogue of OCaml's `h :: t` โ€” same idea, different syntax. 2. Passing a generic `P: Fn` through recursive calls creates an infinite chain of wrapper types at compile time; the `&dyn Fn` inner helper erases the type, breaking the cycle. 3. The iterator version (`filter`) is the most idiomatic and is what `Iterator::filter` does โ€” studying the from-scratch versions illuminates the standard library. 4. All three implementations produce the same result; the fold form is the most allocation-friendly for in-order traversal. 5. OCaml partial application (`let evens = filter f`) is concise; Rust achieves the same by capturing the predicate in a closure.