# Predicate Design Analysis
## Overview
This document analyzes the design of Predicate (predicate) from the perspective of its essential semantics, exploring its main uses and core value, and discussing reasonable design approaches.
The core function of Predicate is **to determine whether a value satisfies specific conditions**, similar to Java's `Predicate<T>` interface and Rust standard library's `Fn(&T) -> bool`. This document will analyze in depth why many seemingly reasonable designs are actually over-engineered, and propose simplified solutions that better align with semantics.
---
## 1. The Essential Semantics of Predicate
### 1.1 What is a Predicate?
**Core semantics of Predicate (predicate)**:
> **Determine whether a value satisfies a certain condition, returning a boolean value. This is a pure "read-only judgment" operation that should have no side effects.**
This is similar to predicate logic in mathematics:
- ✅ **Condition judgment**: Read value properties and make true/false judgments
- ✅ **No side effects**: Do not modify the value being judged
- ✅ **Repeatability**: Same input should produce same result
- ✅ **Determinism**: Judgment logic should be deterministic and predictable
**Comparison with other functional abstractions**:
| **Predicate** | `&T` | `bool` | ❌ | ❌ | Filtering, validation, condition judgment |
| **Consumer** | `&T` | `()` | ❌ | ✅ | Observation, logging, statistics, accumulation |
| **Function** | `&T` | `R` | ❌ | ❌ | Transformation, mapping, computation |
**Key insights**:
- Predicate's semantics is "judgment", and judgment itself should not change anything
- If a "predicate" changes state during judgment, it probably shouldn't be called a predicate
### 1.2 Main Uses of Predicate
| **Filtering/Screening** | Used with `filter()` and other iterator methods | `vec.into_iter().filter(predicate)` |
| **Condition Validation** | Form validation, data validation | `validator.test(&user_input)` |
| **Logical Composition** | Building complex judgment conditions | `is_adult.and(&has_license)` |
| **Strategy Pattern** | Saving judgment logic as strategy | `rules.insert("age", predicate)` |
| **Configuration-driven** | Saving validation rules in configuration center | `config.get_validator("email")` |
### 1.3 Core Value of Predicate
**Temporary judgment vs. Saving logic**:
```rust
// ❌ No need for Predicate: temporary judgment once
if x > 0 && x % 2 == 0 {
println!("positive and even");
}
// ✅ Need Predicate: save judgment logic for reuse
let is_valid = BoxPredicate::new(|x: &i32| *x > 0 && x % 2 == 0);
let result1 = values1.into_iter().filter(|x| is_valid.test(x));
let result2 = values2.into_iter().filter(|x| is_valid.test(x));
```
**The value of Predicate lies in**:
1. **Saving judgment logic**: Encapsulate judgment conditions as reusable objects
2. **Lazy execution**: Execute judgment only when needed
3. **Logical composition**: Build complex conditions through `and`, `or`, `not`
4. **Simplified interface**: Improve code readability as type constraints
---
## 2. Core Design Decisions
### 2.1 Why PredicateOnce is Not Needed? ❌
#### Semantic Contradiction
The essence of Predicate is "judgment", and judgment operations should naturally be **repeatable and side-effect-free**.
```rust
// 🤔 Is this reasonable?
let is_positive = BoxPredicateOnce::new(|x: &i32| *x > 0);
assert!(is_positive.test_once(&5)); // First judgment
// is_positive can't be used anymore! Why can "is positive" only be used once?
```
**Comparison with Consumer**:
- `ConsumerOnce` makes sense: consume a value, it's gone after consumption (like sending messages, closing resources)
- `PredicateOnce` is confusing: judge a value, why is the predicate gone after judgment?
#### Lack of Real Use Cases
The so-called "use cases" are all forced:
1. **Closure capturing non-cloneable resources** - This is not a typical Predicate scenario, more like special resource management
2. **Type system expressiveness** - Expressing for the sake of expressing, not real needs
3. **Lazy execution** - Using `FnOnce` closures directly is simpler
#### Blurred Boundary with PredicateMut
```rust
// PredicateMut can do everything PredicateOnce can do
```rust
// ❌ Using PredicateMut
let mut count = 0;
*x > 0
});
// ✅ Using Predicate + Cell
let count = Cell::new(0);
*x > 0
});
```
**Scenario 3: Thread-safe state**
```rust
// ❌ Using ArcPredicateMut
let counter = Arc::new(Mutex::new(0));
*count += 1;
*x > 0
});
// ✅ Using ArcPredicate + Mutex (same implementation)
let counter = Arc::new(Mutex::new(0));
*count += 1;
*x > 0
});
```
#### Why Interior Mutability is Better?
| **User Code** | `let mut pred = ...` | `let pred = ...` |
| **Call Method** | `pred.test_mut(&x)` | `pred.test(&x)` |
| **Semantics** | "This predicate will change" ❌ | "This predicate is judgment" (internal optimization) ✅ |
| **Flexibility** | Cannot use in immutable context | Can use anywhere |
| **Implementation Complexity** | Needs additional trait | Unified using Predicate |
#### Comparison with Standard Library Design
Rust standard library extensively uses interior mutability to hide implementation details:
```rust
// Arc::clone internally modifies reference count, but interface is &self
pub fn clone(&self) -> Self {
// Atomically increment reference count (interior mutability)
}
// RefCell provides interior mutability
let cell = RefCell::new(5);
let borrowed = cell.borrow_mut(); // &self → &mut T
```
**Conclusion**: `PredicateMut` is unnecessary complexity and should be **removed**. All state-requiring scenarios can be solved with interior mutability.
---
### 2.3 Simplified Core Design
Based on the above analysis, the Predicate module only needs:
```rust
/// Predicate - determines whether a value satisfies a condition
pub trait Predicate<T> {
/// Test whether a value satisfies the condition
///
/// Uses &self, won't change the predicate itself (from user perspective).
/// If internal state is needed (like caching), use RefCell, Cell, or Mutex.
fn test(&self, value: &T) -> bool;
// Type conversion methods
fn into_box(self) -> BoxPredicate<T> where ...;
fn into_rc(self) -> RcPredicate<T> where ...;
fn into_arc(self) -> ArcPredicate<T> where ...;
}
```
**Just this one trait!** Simple, clear, and semantically correct.
---
## 3. Implementation Approach Comparison
### Approach 1: Type Alias + Static Composition Methods
**Core idea**:
```rust
pub type Predicate<T> = Box<dyn Fn(&T) -> bool>;
pub type ArcPredicate<T> = Arc<dyn Fn(&T) -> bool + Send + Sync>;
pub struct Predicates;
impl Predicates {
pub fn and<T>(first: ..., second: ...) -> Predicate<T> { ... }
pub fn or<T>(first: ..., second: ...) -> Predicate<T> { ... }
}
```
**Advantages**:
- ✅ **Ultra-simple API**: Direct call `pred(&value)`
- ✅ **Zero mental burden**: Type aliases are completely transparent
- ✅ **Perfect integration with standard library**: Can be used directly with `filter` methods
- ✅ **Simple implementation**: Less code, easy to understand
**Disadvantages**:
- ❌ **Cannot extend**: Cannot add fields, implement traits
- ❌ **Low type distinction**: Equivalent to `Box<dyn Fn>`
- ❌ **Cannot implement method chaining**: Can only nest calls
- ❌ **Need to maintain multiple APIs**: Predicate, ArcPredicate, RcPredicate each have separate utility classes
**Applicable scenarios**: Rapid prototyping, simple applications, pursuing ultra-simple API
---
### Approach 2: Struct Encapsulation + Instance Methods
**Core idea**:
```rust
pub struct Predicate<T> {
inner: Box<dyn Fn(&T) -> bool>,
name: Option<String>, // Can add metadata
}
impl<T> Predicate<T> {
pub fn test(&self, value: &T) -> bool { ... }
pub fn and(self, other: ...) -> Self { ... } // Consumes self
pub fn or(self, other: ...) -> Self { ... }
}
pub struct ArcPredicate<T> {
inner: Arc<dyn Fn(&T) -> bool + Send + Sync>,
name: Option<String>,
}
// Similar implementation...
```
**Advantages**:
- ✅ **Elegant method chaining**: `.and().or().not()` fluent calls
- ✅ **Strong extensibility**: Can add fields, implement traits
- ✅ **Type safety**: Independent types, clear semantics
**Disadvantages**:
- ❌ **Cannot call directly**: Must use `pred.test(&value)`
- ❌ **Need multiple independent implementations**: Predicate, ArcPredicate, RcPredicate code duplication
- ❌ **Ownership issues**: Box version method chaining consumes self, Arc version needs explicit clone
**Applicable scenarios**: Need metadata, need method chaining, object-oriented style
---
### Approach 3: Trait Abstraction + Multiple Implementations ⭐ (Recommended)
**Core idea**:
```rust
// 1. Unified Predicate trait (minimal)
pub trait Predicate<T> {
fn test(&self, value: &T) -> bool;
// Only test and into_* conversion methods, no logical composition
}
// 2. Implement Predicate for closures
impl<T, F> Predicate<T> for F where F: Fn(&T) -> bool {
fn test(&self, value: &T) -> bool { self(value) }
}
// 3. Extension trait providing composition methods for closures
pub trait FnPredicateOps<T>: Fn(&T) -> bool {
fn and<P>(self, other: P) -> BoxPredicate<T> { ... }
fn or<P>(self, other: P) -> BoxPredicate<T> { ... }
}
// 4. Three specific implementations
pub struct BoxPredicate<T> { /* Box<dyn Fn> */ }
impl<T> BoxPredicate<T> {
pub fn and<P>(self, other: P) -> BoxPredicate<T> { ... } // Consumes self
}
pub struct ArcPredicate<T> { /* Arc<dyn Fn + Send + Sync> */ }
impl<T> ArcPredicate<T> {
pub fn and(&self, other: &ArcPredicate<T>) -> ArcPredicate<T> { ... } // Borrows &self
}
pub struct RcPredicate<T> { /* Rc<dyn Fn> */ }
impl<T> RcPredicate<T> {
pub fn and(&self, other: &RcPredicate<T>) -> RcPredicate<T> { ... } // Borrows &self
}
```
**Advantages**:
- ✅ **Unified trait interface**: All types implement the same `Predicate<T>`
- ✅ **Extremely clear semantics**: `BoxPredicate`, `ArcPredicate`, `RcPredicate` names are self-documenting
- ✅ **Complete ownership model**: Box (single), Arc (shared + thread-safe), Rc (shared + single-thread)
- ✅ **Type preservation**: `ArcPredicate.and()` returns `ArcPredicate`, maintaining cloneable characteristics
- ✅ **Elegant API**: Arc/Rc use `&self`, no need for explicit clone
- ✅ **Strongest extensibility**: Can add new types, fields, traits
- ✅ **Consistent with Rust standard library**: Similar to Box/Arc/Rc smart pointer design
**Disadvantages**:
- ❌ **Cannot call directly**: Still need `.test()`
- ❌ **Slightly higher learning cost**: Need to understand differences between three implementations
- ❌ **High implementation cost**: Need to implement separately for three structs
**Applicable scenarios**: Library development, large projects, long-term maintenance, multi-scenario support
---
## 4. Summary of Three Approaches
| **Call Method** | `pred(&x)` ✅ | `pred.test(&x)` | `pred.test(&x)` |
| **Semantic Clarity** | 🟡 Medium | 🟢 Good | 🟢 **Excellent** ✨ |
| **Unified Interface** | ❌ Multiple independent APIs | ❌ Multiple independent structs | ✅ **Unified trait** ✨ |
| **Ownership Model** | Box + Arc (two) | Box + Arc (two) | Box + Arc + Rc (three) ✅ |
| **Method Chaining** | ❌ Can only nest | ✅ Supported | ✅ **Supported (with type preservation)** ✨ |
| **Extensibility** | ❌ Cannot extend | ✅ Extensible | ✅ **Highly extensible** |
| **Code Simplicity** | ✅ **Ultra-simple** | 🟡 Medium | 🟡 Slightly complex |
| **Learning Cost** | ✅ **Lowest** | 🟡 Medium | 🟡 Slightly high |
| **Maintenance Cost** | 🟡 Medium | 🟡 Medium | ✅ **Low (clear architecture)** |
| **Consistency with Standard Library** | 🟡 Medium | 🟡 Medium | ✅ **Perfect** ✨ |
---
## 5. Final Recommended Design
### 5.1 Core Architecture
```rust
// ============================================================================
// 1. Minimal Predicate trait
// ============================================================================
/// Predicate - determines whether a value satisfies a condition
pub trait Predicate<T> {
/// Test whether a value satisfies the condition
fn test(&self, value: &T) -> bool;
// Type conversion methods
fn into_box(self) -> BoxPredicate<T> where Self: Sized + 'static, T: 'static;
fn into_rc(self) -> RcPredicate<T> where Self: Sized + 'static, T: 'static;
fn into_arc(self) -> ArcPredicate<T> where Self: Sized + Send + Sync + 'static, T: Send + Sync + 'static;
}
// ============================================================================
// 2. Provide extension capabilities for closures
// ============================================================================
/// Implement Predicate trait for closures
impl<T, F> Predicate<T> for F where F: Fn(&T) -> bool {
fn test(&self, value: &T) -> bool { self(value) }
// ...
}
/// Extension trait providing composition methods for closures
pub trait FnPredicateOps<T>: Fn(&T) -> bool + Sized {
fn and<P>(self, other: P) -> BoxPredicate<T> { ... }
fn or<P>(self, other: P) -> BoxPredicate<T> { ... }
fn not(self) -> BoxPredicate<T> { ... }
}
// ============================================================================
// 3. Three specific implementations
// ============================================================================
/// Box implementation - single ownership, consumes self
pub struct BoxPredicate<T> {
function: Box<dyn Fn(&T) -> bool>,
name: Option<String>,
}
impl<T> BoxPredicate<T> {
pub fn and<P>(self, other: P) -> BoxPredicate<T> { ... } // Consumes self
pub fn or<P>(self, other: P) -> BoxPredicate<T> { ... }
pub fn not(self) -> BoxPredicate<T> { ... }
}
/// Arc implementation - thread-safe sharing, borrows &self
pub struct ArcPredicate<T> {
function: Arc<dyn Fn(&T) -> bool + Send + Sync>,
name: Option<String>,
}
impl<T> ArcPredicate<T> {
pub fn and(&self, other: &ArcPredicate<T>) -> ArcPredicate<T> { ... } // Borrows &self
pub fn or(&self, other: &ArcPredicate<T>) -> ArcPredicate<T> { ... }
pub fn not(&self) -> ArcPredicate<T> { ... }
// Provide to_* methods (doesn't consume self)
pub fn to_box(&self) -> BoxPredicate<T> { ... }
pub fn to_rc(&self) -> RcPredicate<T> { ... }
}
/// Rc implementation - single-thread sharing, borrows &self
pub struct RcPredicate<T> {
function: Rc<dyn Fn(&T) -> bool>,
name: Option<String>,
}
impl<T> RcPredicate<T> {
pub fn and(&self, other: &RcPredicate<T>) -> RcPredicate<T> { ... } // Borrows &self
pub fn or(&self, other: &RcPredicate<T>) -> RcPredicate<T> { ... }
pub fn not(&self) -> RcPredicate<T> { ... }
// Provide to_* methods (doesn't consume self)
pub fn to_box(&self) -> BoxPredicate<T> { ... }
}
```
### 5.2 Usage Examples
```rust
// Closures automatically implement Predicate
let is_positive = |x: &i32| *x > 0;
assert!(is_positive.test(&5));
// Closure composition returns BoxPredicate
let combined = is_positive.and(|x: &i32| x % 2 == 0);
assert!(combined.test(&4));
// BoxPredicate - one-time use
let arc_pred = ArcPredicate::new(|x: &i32| *x > 0);
let combined = arc_pred.and(&ArcPredicate::new(|x| x % 2 == 0));
assert!(arc_pred.test(&5)); // Original predicate still usable
// RcPredicate - single-thread reuse, better performance
let rc_pred = RcPredicate::new(|x: &i32| *x > 0);
let combined1 = rc_pred.and(&RcPredicate::new(|x| x % 2 == 0));
let combined2 = rc_pred.or(&RcPredicate::new(|x| *x > 100));
// Interior mutability for caching
use std::cell::RefCell;
let cache = RefCell::new(HashMap::new());
});
cached.test(&5); // No need for mut
```
### 5.3 Type Selection Guide
| One-time use | `BoxPredicate` | Single ownership, no overhead |
| Multi-thread sharing | `ArcPredicate` | Thread-safe, cloneable |
| Single-thread reuse | `RcPredicate` | No atomic operations, better performance |
| Need internal state | Any type + RefCell/Cell/Mutex | Interior mutability |
---
## 6. Summary
### 6.1 Core Design Principles
1. **Predicate is pure judgment**: Doesn't modify input, doesn't modify self (semantically)
2. **Only need one Predicate trait**: Uses `&self`, simple and clear
3. **Remove PredicateOnce**: Violates semantics, lacks real scenarios
4. **Remove PredicateMut**: Interior mutability is completely sufficient
5. **Provide three implementations**: Box/Arc/Rc cover all ownership scenarios
6. **Type names are semantically clear**: BoxPredicate, ArcPredicate, RcPredicate
### 6.2 Why This Design is Best?
**Comparison with over-engineering**:
| **Number of Traits** | 3 (Predicate, PredicateMut, PredicateOnce) | 1 (Predicate) ✅ |
| **Core Semantics** | Confusing (mutable, one-time) | Clear (pure judgment) ✅ |
| **User Mental Burden** | High (which one to use?) | Low (only one) ✅ |
| **State Management** | Needs `&mut self` | Interior mutability ✅ |
| **API Consistency** | Multiple methods (test, test_mut, test_once) | Unified test ✅ |
**Consistency with Consumer design**:
- Consumer **can** modify itself (accumulation, counting are core uses) → ConsumerMut is reasonable
- Predicate **should not** modify itself (pure judgment is core semantics) → PredicateMut is unreasonable
### 6.3 Final Conclusion
For a library project like `qubit-function`:
1. **Adopt Approach 3**: Trait + Multiple implementations
2. **Simplify to single Predicate trait**: Remove Mut and Once variants
3. **Provide three implementations**: BoxPredicate, ArcPredicate, RcPredicate
4. **Use interior mutability**: Use RefCell/Cell/Mutex when state is needed
5. **Document best practices**: Guide users on when to use which type
This design:
- ✅ **Simpler**: Only one core trait
- ✅ **More semantically correct**: Predicate is judgment, shouldn't "change"
- ✅ **More flexible**: No need for `mut`, can be used in more places
- ✅ **Consistent with Rust conventions**: Standard library extensively uses interior mutability patterns
- ✅ **Long-term maintainable**: Clear architecture, clear semantics
**This is a thoughtful, over-engineering-free, back-to-basics elegant solution.**