Date: 2025-11-06 Status: Decision Made - Option B Recommended
This document compares implementation approaches for adding Universal Levenshtein Automata (restricted substitutions) to liblevenshtein-rust and provides a recommendation.
Add RestrictedSubstitution as a 4th variant to the Algorithm enum.
Add substitution_set as an optional field in TransducerBuilder, orthogonal to the Algorithm choice.
Implementation:
pub enum Algorithm {
Standard,
Transposition,
MergeAndSplit,
RestrictedSubstitution, // NEW VARIANT
}
Usage:
let dict = TransducerBuilder::new()
.algorithm(Algorithm::RestrictedSubstitution)
.with_substitution_set(set) // Configure which substitutions
.build_from_iter(words);
Cannot combine algorithms:
RestrictedTransposition, RestrictedMergeAndSplit variants → enum explosionCode duplication:
Inconsistent with paper:
Future inflexibility:
API confusion:
.algorithm(RestrictedSubstitution) but doesn't call .with_substitution_set()?Implementation:
pub struct TransducerBuilder<D> {
algorithm: Algorithm, // Standard, Transposition, MergeAndSplit
substitution_set: Option<SubstitutionSet>, // NEW: None = unrestricted
}
Usage:
// Standard + restricted substitutions
let dict = TransducerBuilder::new()
.algorithm(Algorithm::Standard)
.with_substitution_set(qwerty_set)
.build_from_iter(words);
// Transposition + restricted substitutions
let dict = TransducerBuilder::new()
.algorithm(Algorithm::Transposition)
.with_substitution_set(ocr_set)
.build_from_iter(words);
// Standard + unrestricted (backward compatible)
let dict = TransducerBuilder::new()
.algorithm(Algorithm::Standard)
.build_from_iter(words); // No substitution_set → unrestricted
Composability: Works with ALL existing algorithms
Code reuse:
Aligns with paper:
Future-proof:
Backward compatible:
None substitution set → unrestricted (current behavior)Clear semantics:
None = "all substitutions allowed"Some(set) = "only substitutions in set allowed"Flexible presets:
.with_qwerty_substitutions(), .with_ocr_confusions(), etc.Slightly more complex builder:
TransducerBuilderLess explicit in type system:
Algorithm enum| Criterion | Option A (Enum Variant) | Option B (Configuration) |
|---|---|---|
| Composability | ❌ Cannot combine with Transposition/MergeAndSplit | ✅ Works with all algorithms |
| Code duplication | ❌ Duplicates transition logic per variant | ✅ Single implementation, reused |
| Alignment with paper | ⚠️ Treats as separate algorithm | ✅ Orthogonal to operation type |
| Backward compatibility | ⚠️ New variant, but existing code OK | ✅ Optional field, perfect compat |
| Future extensibility | ❌ Enum explosion for combinations | ✅ Builder pattern scales well |
| API clarity | ✅ Explicit in Algorithm enum | ⚠️ Requires reading builder docs |
| Implementation complexity | 🔴 High (duplicate code paths) | 🟢 Low (add checks to existing) |
| Type safety | ✅ Compiler enforces algorithm choice | ⚠️ Optional field (but semantics clear) |
| Preset convenience | ⚠️ Can provide, but per-algorithm | ✅ Easy builder methods for presets |
| Configuration errors | ❌ Need to handle missing SubstitutionSet | ✅ None = valid default |
Option A:
.algorithm(Algorithm::RestrictedSubstitution)
.with_substitution_set(SubstitutionSet::qwerty())
Option B:
.algorithm(Algorithm::Standard)
.with_substitution_set(SubstitutionSet::qwerty())
// OR
.algorithm(Algorithm::Standard)
.with_qwerty_substitutions()
Winner: Option B (clearer that we're using Standard + restrictions)
Option A:
// CANNOT DO THIS - would need new enum variant
// Algorithm::RestrictedTransposition doesn't exist
// Would require adding it, duplicating all transposition logic
Option B:
.algorithm(Algorithm::Transposition)
.with_substitution_set(SubstitutionSet::ocr_confusions())
Winner: Option B (only option that supports this)
Option A:
// CANNOT DO THIS - would need Algorithm::RestrictedMergeAndSplit
// More enum variants, more duplication
Option B:
.algorithm(Algorithm::MergeAndSplit)
.with_substitution_set(SubstitutionSet::phonetic_english())
Winner: Option B (only option that supports this)
Option A:
.algorithm(Algorithm::Standard) // Existing variant
Option B:
.algorithm(Algorithm::Standard) // No .with_substitution_set() call
Winner: Tie (both maintain backward compatibility)
From Section 2 of "Universal Levenshtein Automata for a Generalization of the Levenshtein Distance":
"We consider a generalization of the Levenshtein distance where substitutions are restricted to pairs in a set $
S \subseteq \Sigma \times \Sigma$."
Key observation: The set S is a parameter to the distance function, not a fundamentally different algorithm.
"The construction of the universal Levenshtein automaton can be extended to handle this restriction in combination with transposition, merge, and split operations."
Interpretation: Restricted substitutions are orthogonal to the choice of operations (transposition, merge, split).
Effort:
Algorithm::RestrictedSubstitution variantStandard algorithmAlgorithm::RestrictedTransposition (duplicate again)Algorithm::RestrictedMergeAndSplit (duplicate again)Total: 🔴 High effort, with ongoing maintenance burden
Effort:
substitution_set: Option<SubstitutionSet> field to builderwith_substitution_set() methodif check in each algorithm's transition functionTotal: 🟢 Low effort, single implementation
Scenario: Bug found in transposition logic
Fix required:
Algorithm::TranspositionAlgorithm::RestrictedTransposition (duplicate code path)Burden: 🔴 High - every bug needs multiple fixes
Scenario: Bug found in transposition logic
Fix required:
Algorithm::Transposition transition functionBurden: 🟢 Low - single fix location
Option A: Would need:
pub enum Algorithm {
Standard,
Transposition,
MergeAndSplit,
RestrictedSubstitution,
WeightedStandard, // NEW
WeightedTransposition, // NEW
WeightedMergeAndSplit, // NEW
RestrictedWeightedStandard, // NEW (combination!)
RestrictedWeightedTransposition, // Enum explosion!
// 🔥 This is unsustainable
}
Option B: Would add:
pub struct TransducerBuilder<D> {
algorithm: Algorithm, // Still just 3 variants
substitution_set: Option<SubstitutionSet>, // Existing
operation_weights: Option<OperationWeights>, // NEW field
}
Winner: Option B (scales to multiple orthogonal features)
Builder pattern best practices:
Examples:
// reqwest (HTTP client)
let client = Client::builder()
.timeout(Duration::from_secs(10)) // Optional config
.build();
// tokio (async runtime)
let rt = Builder::new_multi_thread()
.worker_threads(4) // Optional config
.enable_all() // Feature flag
.build();
Pattern: Option B aligns with Rust ecosystem conventions.
Rationale:
Trade-offs accepted:
substitution_set: Option<SubstitutionSet> to TransducerBuilder.with_substitution_set(set) methodstandard_transition()transposition_transition()merge_split_transition().with_qwerty_substitutions().with_azerty_substitutions().with_dvorak_substitutions().with_ocr_confusions().with_phonetic_english()Idea: Start with Option B, later add Option A if needed.
Analysis:
Decision: Not recommended - stick with Option B exclusively.
Decision: Implement Option B (Configuration-Based)
Next Steps:
Last Updated: 2025-11-06 Status: Decision Made - Option B Selected Implementation: Ready to Begin
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 |