Custom Equatability in Rust
In Rust, equatability is typically implemented using the PartialEq
and Eq
traits. While the default implementation works well for many use cases, there are scenarios where custom implementations are necessary.
In this post, we will explore unique business cases where equatability needs customization, such as ignoring specific fields, handling floating-point precision, or using alternative identifiers. We’ll also see how Rust enables these implementations efficiently.
- Ignoring Certain Fields in Comparisons
- Fuzzy Equality (Tolerance-Based Comparisons)
- Contextual Equatability (Mode-Based Comparisons)
- Lexicographic Equality (Case-Insensitive Comparisons)
- Group-Based Equatability (Multiple Identifiers)
- Final Thoughts
Ignoring Certain Fields in Comparisons
📌 Scenario: In some business logic, fields like IDs, timestamps, or version numbers should not be considered when checking equality. For example, in a user management system, users might be uniquely identified by an ID, but for business operations such as sending notifications or grouping users by attributes, only the name and email should be compared.
🦀 Rust Implementation
#[derive(Debug)]
struct User {
id: u32, // Ignored during comparison
name: String,
email: String,
}
impl PartialEq for User {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.email == other.email
}
}
fn main() {
let user1 = User {
id: 1,
name: String::from("Alice"),
email: String::from("alice@example.com"),
};
let user2 = User {
id: 2, // Different ID, but same name and email
name: String::from("Alice"),
email: String::from("alice@example.com"),
};
let user3 = User {
id: 3,
name: String::from("Bob"),
email: String::from("bob@example.com"),
};
// Comparison ignoring the `id` field
println!("user1 == user2: {}", user1 == user2); // true
println!("user1 == user3: {}", user1 == user3); // false
println!("{:?}", user1);
}
✅ Benefit: Enables equality checks without considering irrelevant fields.
Fuzzy Equality (Tolerance-Based Comparisons)
📌 Scenario: For floating-point comparisons, strict equality often isn’t desirable due to precision errors. These errors occur because floating-point numbers are represented in binary, leading to small rounding discrepancies when performing arithmetic operations. Instead, we check if two numbers are close enough.
🦀 Rust Implementation
#[derive(Debug)]
struct Point {
x: f64,
y: f64,
}
impl PartialEq for Point {
fn eq(&self, other: &Self) -> bool {
(self.x - other.x).abs() < 0.01 && (self.y - other.y).abs() < 0.01
}
}
fn main() {
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = Point { x: 1.005, y: 2.005 }; // Slightly different but within the threshold
let p3 = Point { x: 1.02, y: 2.02 }; // Outside the threshold
// Comparison with tolerance
println!("p1 == p2: {}", p1 == p2); // true
println!("p1 == p3: {}", p1 == p3); // false
}
✅ Benefit: Ensures floating-point comparisons account for minor precision differences.
Contextual Equatability (Mode-Based Comparisons)
📌 Scenario: Objects might need to be compared differently based on business logic. For example, in strict mode, we compare all fields, while in a relaxed mode, we might ignore price differences.
🦀 Rust Implementation
#[derive(Debug)]
struct Product {
name: String,
price: f64,
}
enum ComparisonMode {
Strict,
IgnorePrice,
}
impl Product {
fn equals(&self, other: &Self, mode: ComparisonMode) -> bool {
match mode {
ComparisonMode::Strict => self.name == other.name && self.price == other.price,
ComparisonMode::IgnorePrice => self.name == other.name,
}
}
}
fn main() {
let product1 = Product {
name: String::from("Laptop"),
price: 999.99,
};
let product2 = Product {
name: String::from("Laptop"),
price: 999.99, // Same price
};
let product3 = Product {
name: String::from("Laptop"),
price: 899.99, // Different price
};
let product4 = Product {
name: String::from("Phone"),
price: 799.99, // Different name and price
};
// Comparisons
println!("product1 == product2 (Strict): {}", product1.equals(&product2, ComparisonMode::Strict)); // true
println!("product1 == product3 (Strict): {}", product1.equals(&product3, ComparisonMode::Strict)); // false
println!("product1 == product3 (IgnorePrice): {}", product1.equals(&product3, ComparisonMode::IgnorePrice)); // true
println!("product1 == product4 (IgnorePrice): {}", product1.equals(&product4, ComparisonMode::IgnorePrice)); // false
}
✅ Benefit: Allows for flexible comparison logic based on different contexts.
Lexicographic Equality (Case-Insensitive Comparisons)
📌 Scenario: String-based comparisons sometimes need to be case-insensitive, such as when comparing usernames or product names.
🦀 Rust Implementation
#[derive(Debug)]
struct Person {
name: String,
}
impl PartialEq for Person {
fn eq(&self, other: &Self) -> bool {
self.name.to_lowercase() == other.name.to_lowercase()
}
}
fn main() {
let person1 = Person {
name: String::from("Alice"),
};
let person2 = Person {
name: String::from("alice"), // Same name but different case
};
let person3 = Person {
name: String::from("Bob"), // Different name
};
// Comparison ignoring case
println!("person1 == person2: {}", person1 == person2); // true
println!("person1 == person3: {}", person1 == person3); // false
}
✅ Benefit: Handles case-insensitive comparisons without requiring manual normalization.
Group-Based Equatability (Multiple Identifiers)
📌 Scenario: Sometimes, two objects should be considered equal if they share at least one common identifier, even if they have different unique keys (e.g., multiple UPC codes for the same product).
🦀 Rust Implementation
#[derive(Debug)]
struct Product {
upcs: Vec<String>, // Different codes, but same product
name: String,
}
impl PartialEq for Product {
fn eq(&self, other: &Self) -> bool {
self.upcs.iter().any(|upc| other.upcs.contains(upc))
}
}
fn main() {
let product1 = Product {
upcs: vec![String::from("12345"), String::from("67890")],
name: String::from("Laptop"),
};
let product2 = Product {
upcs: vec![String::from("67890"), String::from("11111")], // Overlapping UPC: "67890"
name: String::from("Laptop"),
};
let product3 = Product {
upcs: vec![String::from("99999"), String::from("88888")], // No matching UPC
name: String::from("Laptop"),
};
// Comparison based on shared UPCs
println!("product1 == product2: {}", product1 == product2); // true (common UPC: "67890")
println!("product1 == product3: {}", product1 == product3); // false (no common UPC)
}
✅ Benefit: Supports alternative identifiers for equality checks.
Final Thoughts
Before wrapping up, let’s recap some key takeaways: Rust allows for flexible equatability customizations, including ignoring fields, fuzzy comparisons, contextual equality, and alternative identifiers. These techniques enable more accurate and meaningful equality checks for different business cases.
Rust’s trait-based approach to equatability allows developers to customize equality logic based on business needs. Whether it’s ignoring certain fields, implementing fuzzy comparisons, handling context-dependent equality, or using alternative identifiers, Rust provides the flexibility to achieve it efficiently.
By leveraging PartialEq
and other techniques, we can ensure that equality checks align with our application’s
logic instead of being constrained by default implementations.