044 — Option Filter
Tutorial Video
Text description (accessibility)
This video demonstrates the "044 — Option Filter" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. `Option::filter` applies a predicate to the value inside a `Some`, turning it into `None` if the predicate fails. Key difference from OCaml: 1. **Predicate argument**: Rust's filter closure receives `&T` (reference to the inner value). OCaml's filter function receives `T` directly. Be careful with the reference in Rust: `filter(|&x| x > 0)` pattern
Tutorial
The Problem
Option::filter applies a predicate to the value inside a Some, turning it into None if the predicate fails. It is the conditional guard operation for Option: "keep this value only if it satisfies this condition". Combined with map and and_then, filter completes the basic Option toolkit.
This pattern appears in validation pipelines: parse_int(s).filter(|&x| x >= 0).filter(|&x| x < 100) — parse a string, then validate the range, producing None at any failure point. It is a declarative alternative to nested if statements on unwrapped values.
🎯 Learning Outcomes
opt.filter(|x| predicate(x)) to discard values not meeting a conditionfilter with map and and_then for validation pipelinesfilter never introduces Some — it can only remove itfilter as a guard in option chains to enforce preconditionsOption::filter(pred) to return None when the predicate fails on a Some(x) value.filter().map().and_then() for declarative option processing without explicit matchCode Example
#![allow(clippy::all)]
// Option::filter: keep the value only if it satisfies a predicate, else None.
pub fn filter_positive_even(opt: Option<i32>) -> Option<i32> {
opt.filter(|&x| x > 0).filter(|&x| x % 2 == 0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_positive_even_passes() {
assert_eq!(filter_positive_even(Some(4)), Some(4));
}
#[test]
fn test_negative_fails() {
assert_eq!(filter_positive_even(Some(-4)), None);
}
#[test]
fn test_odd_fails() {
assert_eq!(filter_positive_even(Some(3)), None);
}
#[test]
fn test_none_stays_none() {
assert_eq!(filter_positive_even(None), None);
}
}Key Differences
&T (reference to the inner value). OCaml's filter function receives T directly. Be careful with the reference in Rust: filter(|&x| x > 0) pattern-matches the reference.filter as bind**: opt.filter(pred) is equivalent to opt.and_then(|x| if pred(&x) { Some(x) } else { None }). Understanding this connection shows filter is not a primitive — it is derivable.Option.filter availability**: OCaml's stdlib Option module has never included filter — it must be derived from bind (see above). Rust has always had Option::filter as a first-class method.retain analogy**: Option::filter is analogous to Vec::retain — both remove elements that fail a predicate. retain is in-place; filter produces a new Option.Option::filter semantics:** opt.filter(pred) returns None if opt is None or if the predicate returns false. It is equivalent to opt.and_then(|x| if pred(&x) { Some(x) } else { None }).Option.filter; derive it from Option.bind (see above) — the resulting semantics are identical.opt.filter(pred).map(transform) reads as "keep the value if it passes the predicate, then transform it." This is a common pattern for conditional transformation.option_filter is particularly useful for validating optional inputs — keep the value only if it's in a valid range or satisfies a constraint.OCaml Approach
Unlike Option.map and Option.bind, Option.filter is not part of OCaml's stdlib Option module — it must be derived from bind: let option_filter pred opt = Option.bind opt (fun x -> if pred x then Some x else None). Chaining with pipe: opt |> option_filter (fun x -> x > 0) |> Option.map (fun x -> x * 2). Filter is a special case of bind, which is why the stdlib omits it as a separate primitive.
Full Source
#![allow(clippy::all)]
// Option::filter: keep the value only if it satisfies a predicate, else None.
pub fn filter_positive_even(opt: Option<i32>) -> Option<i32> {
opt.filter(|&x| x > 0).filter(|&x| x % 2 == 0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_positive_even_passes() {
assert_eq!(filter_positive_even(Some(4)), Some(4));
}
#[test]
fn test_negative_fails() {
assert_eq!(filter_positive_even(Some(-4)), None);
}
#[test]
fn test_odd_fails() {
assert_eq!(filter_positive_even(Some(3)), None);
}
#[test]
fn test_none_stays_none() {
assert_eq!(filter_positive_even(None), None);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_positive_even_passes() {
assert_eq!(filter_positive_even(Some(4)), Some(4));
}
#[test]
fn test_negative_fails() {
assert_eq!(filter_positive_even(Some(-4)), None);
}
#[test]
fn test_odd_fails() {
assert_eq!(filter_positive_even(Some(3)), None);
}
#[test]
fn test_none_stays_none() {
assert_eq!(filter_positive_even(None), None);
}
}
Deep Comparison
OCaml vs Rust: Option Filter
Overview
See the example.rs and example.ml files for detailed implementations.
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Type system | Hindley-Milner | Ownership + traits |
| Memory | GC | Zero-cost abstractions |
| Mutability | Explicit ref | mut keyword |
| Error handling | Option/Result | Result<T, E> |
See README.md for detailed comparison.
Exercises
validate_age(age: Option<i32>) -> Option<i32> that filters to [0, 150] using two chained filter calls.non_empty(s: Option<String>) -> Option<String> that returns None for Some("") and passes through non-empty strings. Use filter(|s| !s.is_empty()).square_if_positive(opt: Option<i32>) -> Option<i32> that squares the value only if it is positive, returning None otherwise. Combine filter and map.filter_positive_even(opt: Option<i32>) -> Option<i32> using two chained .filter() calls — one for positive, one for even.filter_map_option<T, U>(opt: Option<T>, pred: impl Fn(&T) -> bool, transform: impl Fn(T) -> U) -> Option<U> combining filter and map in one step.