Date: 2025-11-11
Question: Should we implement Transducer<D, P = Unrestricted> with a policy type parameter?
Answer: YES - It's the better long-term solution
After thorough analysis, Option 1 is superior to Option 2 for the long-term. While it requires more upfront work, it provides:
Key Insight: Rust's default type parameters mean existing code continues to work unchanged:
// OLD CODE - still works!
let t = Transducer::new(dict, Algorithm::Standard);
// Type inference: Transducer<DynamicDawg, Unrestricted>
// NEW CODE - opt-in to custom policy
let t = Transducer::with_policy(dict, Algorithm::Standard, policy);
// Explicit type: Transducer<DynamicDawg, MyPolicy>
Current:
pub struct Transducer<D: Dictionary> {
dictionary: D,
algorithm: Algorithm,
}
Proposed:
pub struct Transducer<D: Dictionary, P: SubstitutionPolicy = Unrestricted> {
dictionary: D,
algorithm: Algorithm,
policy: P, // Added field - zero bytes for Unrestricted
}
Impact:
Current (2 blocks):
impl<D: Dictionary> Transducer<D> {
// 14 methods
}
impl<D> Transducer<D>
where
D: MappedDictionary,
D::Node: MappedDictionaryNode<Value = D::Value>,
{
// 3 methods (value-filtered)
}
Proposed:
// Block 1: Methods for Unrestricted policy (backward compatible)
impl<D: Dictionary> Transducer<D, Unrestricted> {
pub fn new(dictionary: D, algorithm: Algorithm) -> Self {
Self {
dictionary,
algorithm,
policy: Unrestricted,
}
}
pub fn standard(dictionary: D) -> Self { ... }
pub fn with_transposition(dictionary: D) -> Self { ... }
pub fn with_merge_split(dictionary: D) -> Self { ... }
// ... other constructors
}
// Block 2: Methods for all policies (generic)
impl<D: Dictionary, P: SubstitutionPolicy> Transducer<D, P> {
pub fn with_policy(dictionary: D, algorithm: Algorithm, policy: P) -> Self {
Self { dictionary, algorithm, policy }
}
pub fn query(&self, term: &str, max_distance: usize) -> QueryIterator<D::Node, String> {
QueryIterator::with_substring_mode(
&self.dictionary,
term,
max_distance,
self.algorithm,
false,
)
}
// ... all 14 query methods - UNCHANGED signatures
}
// Block 3: Value-filtered methods (generic over policy)
impl<D, P> Transducer<D, P>
where
D: MappedDictionary,
D::Node: MappedDictionaryNode<Value = D::Value>,
P: SubstitutionPolicy,
{
pub fn query_filtered<F>(...) -> ValueFilteredQueryIterator<D::Node, F> { ... }
pub fn query_by_value_set(...) -> ValueSetFilteredQueryIterator<D::Node, D::Value> { ... }
}
Impact:
with_policy() constructor)Current:
pub struct QueryIterator<N: DictionaryNode, R: QueryResult = String> {
// ... uses Unrestricted internally
}
No change needed! Query iterators already use Unrestricted internally.
Policy-specific matching belongs in a separately measured transition-logic
treatment.
Alternative (if we want policy in iterators):
pub struct QueryIterator<N: DictionaryNode, R: QueryResult = String, P: SubstitutionPolicy = Unrestricted> {
policy: P, // Added
// ...
}
But this is NOT needed yet since we're not using policy in logic.
Current: QueryBuilder - no changes needed since it's a separate builder pattern.
Files to update:
examples/*.rs - ~5 files, no changes needed (default works)README.md - Add section on custom policies# Policy section to struct docsTotal: ~20-30 lines of new documentation
Reality: Rust's default type parameters maintain perfect backward compatibility! ✅
// std::collections::HashMap
pub struct HashMap<K, V, S = RandomState> { ... }
// Users write:
let map = HashMap::new(); // ← Type: HashMap<K, V, RandomState>
// Not: HashMap::<K, V, RandomState>::new()
// Library code
pub struct Transducer<D: Dictionary, P: SubstitutionPolicy = Unrestricted> { ... }
impl<D: Dictionary> Transducer<D, Unrestricted> {
pub fn new(dictionary: D, algorithm: Algorithm) -> Self { ... }
}
// User code (UNCHANGED)
let t = Transducer::new(dict, Algorithm::Standard);
// ^^^^^^^^^^^^^^^^
// Type inference fills in: Transducer<DynamicDawg, Unrestricted>
Test: Compile existing user code with new generic definition
Result: Zero errors because:
new() is impl<D> Transducer<D, Unrestricted> - returns concrete type= Unrestricted means users never write PProof:
// All existing code patterns work unchanged:
let t1 = Transducer::new(dict, Algorithm::Standard);
let t2 = Transducer::standard(dict);
let t3 = Transducer::with_transposition(dict);
// Return types are identical to before:
// Transducer<DynamicDawg> (old)
// Transducer<DynamicDawg, Unrestricted> (new - but transparent)
// OLD (Option 2): Policy passed at query time
let results1 = transducer.query("test", 1); // Uses Unrestricted
let results2 = transducer.query_with_policy("test", 1, custom_policy);
// NEW (Option 1): Policy is part of type
let t_unrestricted: Transducer<_, Unrestricted> = Transducer::new(...);
let t_custom: Transducer<_, MyPolicy> = Transducer::with_policy(..., my_policy);
let results1 = t_unrestricted.query("test", 1); // Type enforces Unrestricted
let results2 = t_custom.query("test", 1); // Type enforces MyPolicy
Benefit: Compile-time guarantee about which policy is used.
// OLD (Option 2): Policy passed per-query
for query in queries {
transducer.query_with_policy(query, 1, policy); // Pass policy 1000x
}
// NEW (Option 1): Policy stored once
let transducer = Transducer::with_policy(dict, alg, policy);
for query in queries {
transducer.query(query, 1); // No extra parameter
}
Benefit:
// OLD (Option 2): Verbose with custom policy
let results = transducer.query_with_policy("test", 1, my_policy);
let results = transducer.query_ordered_with_policy("test", 2, my_policy);
let results = transducer.query_filtered_with_policy("test", 1, my_policy, filter);
// NEW (Option 1): Clean, consistent
let transducer = Transducer::with_policy(dict, alg, my_policy);
let results = transducer.query("test", 1);
let results = transducer.query_ordered("test", 2);
let results = transducer.query_filtered("test", 1, filter);
Benefit: Single API, no duplication of methods.
Option 1 follows std::collections pattern:
HashMap<K, V, S = RandomState>HashSet<T, S = RandomState>BTreeMap<K, V> (no default, but generic structure same)Benefit: Familiar to Rust developers, idiomatic.
P: SubstitutionPolicy = Unrestricted to struct (1 line)policy: P field (1 line)impl blocks to impl<D, P> (2 lines)with_policy() constructor (5 lines)new() to set policy: Unrestricted (1 line)Total: ~10 lines of code, 15 minutes
_with_policy() method variants (200+ lines)Total: 0 lines now, 200+ lines later, ongoing maintenance burden
Rebuttal: Default parameters hide complexity. Users never see P unless they want to.
Rebuttal: NOT a breaking change - default parameters maintain backward compatibility. Proven by HashMap in std.
Rebuttal: Only for users who explicitly use custom policies. For 99% of users (Unrestricted), errors are identical.
Rebuttal:
_with_policy() methods)query_with_policy(), etc.)pub struct Transducer<D: Dictionary, P: SubstitutionPolicy = Unrestricted> {
dictionary: D,
algorithm: Algorithm,
policy: P,
}
impl<D: Dictionary> Transducer<D, Unrestricted> {
pub fn new(dictionary: D, algorithm: Algorithm) -> Self {
Self { dictionary, algorithm, policy: Unrestricted }
}
// ... other constructors (standard, with_transposition, etc.)
}
impl<D: Dictionary, P: SubstitutionPolicy> Transducer<D, P> {
pub fn with_policy(dictionary: D, algorithm: Algorithm, policy: P) -> Self {
Self { dictionary, algorithm, policy }
}
// ... all query methods (signatures unchanged)
}
impl<D, P> Transducer<D, P>
where
D: MappedDictionary,
D::Node: MappedDictionaryNode<Value = D::Value>,
P: SubstitutionPolicy,
{
// ... value-filtered methods
}
cargo test --lib
# Should pass - no API changes visible to tests
Check if TransducerBuilder needs changes - likely just add policy field.
Add examples of custom policy usage to README.
Total Time: ~15 minutes core work, +15 minutes docs later
Option 1 is objectively superior because:
Recommendation: Implement Option 1 immediately. The initial analysis incorrectly assumed it would break compatibility, but Rust's default parameters solve that issue completely.
Proceed with Option 1 implementation:
Transducer<D, P = Unrestricted> structwith_policy() constructorExpected outcome: Zero test failures, zero user code breakage, clean generic API ready for future policy logic implementation.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |