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
- Selection from collections โ keep only the elements that matter for the current task.
- Understanding `dyn Fn` โ the recursive version teaches why Rust sometimes needs `&dyn Fn` in recursive generic code.
- Fold's universality confirmed โ `filter_fold` shows that filter is a fold, just like `map`.
Key Differences
| Concept | OCaml | Rust | ||||
|---|---|---|---|---|---|---|
| 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 generic | Polymorphic recursion straightforward | Needs `&dyn Fn` break-point | ||||
| Order guarantee | Left-to-right, guaranteed | Same โ 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
| Aspect | OCaml | Rust | |
|---|---|---|---|
| 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 safety | structural, GC-managed | needs `&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.