Type System
Fugue-evo uses Rust's type system to ensure correctness and provide good developer experience.
Generic Algorithm Builders
Algorithm builders use extensive generics:
pub struct SimpleGABuilder<G, F, S, C, M, Fit, Term> {
// G: Genome type
// F: Fitness value type
// S: Selection operator
// C: Crossover operator
// M: Mutation operator
// Fit: Fitness function
// Term: Termination criterion
}
Why So Many Generics?
Type safety: Each component's compatibility is checked at compile time.
// This compiles: SBX works with RealVector
SimpleGABuilder::<RealVector, f64, _, _, _, _, _>::new()
.crossover(SbxCrossover::new(20.0))
// This wouldn't compile: SBX doesn't implement CrossoverOperator<BitString>
SimpleGABuilder::<BitString, f64, _, _, _, _, _>::new()
.crossover(SbxCrossover::new(20.0)) // Compile error!
Type Inference
Rust often infers generic parameters:
// Full explicit types (verbose)
SimpleGABuilder::<
RealVector,
f64,
TournamentSelection,
SbxCrossover,
PolynomialMutation,
Sphere,
MaxGenerations,
>::new()
// With inference (same thing, cleaner)
SimpleGABuilder::<RealVector, f64, _, _, _, _, _>::new()
.selection(TournamentSelection::new(3))
.crossover(SbxCrossover::new(20.0))
// Types inferred from usage
Trait Bounds
Genome Constraints
pub trait EvolutionaryGenome:
Clone // Population copying
+ Send // Thread safety
+ Sync // Thread safety
+ Serialize // Checkpointing
+ DeserializeOwned
{
// ...
}
Operator Constraints
pub trait SelectionOperator<G>: Send + Sync {
fn select<R: Rng>(&self, population: &[(G, f64)], rng: &mut R) -> usize;
}
The Send + Sync bounds enable parallel evaluation.
Fitness Constraints
pub trait FitnessValue:
Clone // Result copying
+ PartialOrd // Comparison for selection
+ Send + Sync // Thread safety
{
fn to_f64(&self) -> f64;
}
Associated Types
Some traits use associated types for output:
pub trait Fitness<G>: Send + Sync {
type Value: FitnessValue; // Associated type
fn evaluate(&self, genome: &G) -> Self::Value;
}
// Usage
impl Fitness<RealVector> for Sphere {
type Value = f64; // Sphere returns f64
// ...
}
impl Fitness<RealVector> for MultiObjective {
type Value = ParetoFitness; // Multi-objective returns ParetoFitness
// ...
}
Error Handling
Result Types
Operations that can fail return Result:
pub type EvoResult<T> = Result<T, EvoError>;
pub fn build(self) -> EvoResult<SimpleGA<G, F, S, C, M, Fit, Term>>;
Error Types
#[derive(Debug, thiserror::Error)]
pub enum EvoError {
#[error("Configuration error: {0}")]
Configuration(String),
#[error("Genome error: {0}")]
Genome(#[from] GenomeError),
#[error("Operator error: {0}")]
Operator(String),
// ...
}
Marker Traits
Some traits are markers for capabilities:
// Indicates genome supports bounds
pub trait BoundedGenome: EvolutionaryGenome {
fn clamp_to_bounds(&mut self, bounds: &MultiBounds);
}
// Operators that respect bounds
pub trait BoundedMutationOperator<G: BoundedGenome>: MutationOperator<G> {
fn mutate_bounded<R: Rng>(
&self,
genome: &mut G,
bounds: &MultiBounds,
rng: &mut R,
);
}
Phantom Types
For type-level information without runtime cost:
pub struct Population<G, F> {
individuals: Vec<Individual<G>>,
_fitness: PhantomData<F>, // Track fitness type without storing
}
Conditional Compilation
Features enable/disable functionality:
#[cfg(feature = "parallel")]
impl<G, F, Fit> Population<G, F>
where
G: EvolutionaryGenome,
F: FitnessValue,
Fit: Fitness<G, Value = F>,
{
pub fn evaluate_parallel(&mut self, fitness: &Fit) {
// Rayon parallel implementation
}
}
Type Aliases
For common patterns:
// Convenience aliases
pub type RealGA = SimpleGA<RealVector, f64, TournamentSelection, SbxCrossover, PolynomialMutation, _, MaxGenerations>;
pub type BinaryGA = SimpleGA<BitString, f64, TournamentSelection, UniformCrossover, BitFlipMutation, _, MaxGenerations>;
Best Practices
1. Let Inference Work
// Good: minimal type annotations
let ga = SimpleGABuilder::new()
.bounds(bounds)
.fitness(Sphere::new(10))
// types inferred
// Verbose: unnecessary annotations
let ga: SimpleGA<RealVector, f64, ...> = SimpleGABuilder::new()
2. Use Concrete Types in Applications
// In library code: generic
fn run_optimization<G, F, Fit>(fitness: &Fit) -> EvoResult<G>
where
G: EvolutionaryGenome,
F: FitnessValue,
Fit: Fitness<G, Value = F>,
// In application code: concrete
fn optimize_my_problem() -> EvoResult<RealVector> {
let fitness = MyFitness::new();
// ...
}
3. Understand Error Messages
Generic-heavy code can produce complex error messages. Key hints:
- "trait bound not satisfied" = wrong operator for genome type
- "cannot infer type" = add type annotations
- "lifetime" issues = usually Send/Sync bounds