Skip to main content

Module handler

Module handler 

Source
Expand description

§Handler System

§Overview

Fugue’s handler system solves a fundamental problem in probabilistic programming: how to separate model specification from execution strategy. The handler architecture enables the same probabilistic model to be executed in radically different ways—prior sampling, trace replay, scoring, MCMC—without changing a single line of model code.

This system implements the algebraic effects pattern with full type safety, where:

  • Model<A> describes probabilistic computations as data structures
  • Handler trait defines interpretation of probabilistic effects
  • run(handler, model) executes the interpretation

The key innovation is type-specific effect handling: instead of forcing all distributions through f64, handlers preserve natural types (bool, u64, usize, f64) throughout the execution pipeline.

§Usage Examples

§Basic Execution Pattern


// Define model once
let model = sample(addr!("coin"), Bernoulli::new(0.6).unwrap())  // Returns bool!
    .bind(|heads| {
        if heads {  // Natural boolean logic
            sample(addr!("reward"), Normal::new(10.0, 2.0).unwrap())
        } else {
            sample(addr!("penalty"), Normal::new(-5.0, 1.0).unwrap())
        }
    });

// Execute with different handlers for different purposes
let mut rng = StdRng::seed_from_u64(42);

// Create the model function to avoid clone issues
let make_model = || {
    sample(addr!("coin"), Bernoulli::new(0.6).unwrap())
        .bind(|heads| {
            if heads {
                sample(addr!("reward"), Normal::new(10.0, 2.0).unwrap())
            } else {
                sample(addr!("penalty"), Normal::new(-5.0, 1.0).unwrap())
            }
        })
};

// 1. Prior sampling - generate random execution
let (value1, trace1) = runtime::handler::run(
    PriorHandler { rng: &mut rng, trace: Trace::default() },
    make_model()
);
println!("Prior sample: {}, coin was: {:?}",
    value1, trace1.choices[&addr!("coin")]);

// 2. Replay - reuse choices from trace1, sample any missing
let (value2, trace2) = runtime::handler::run(
    ReplayHandler {
        rng: &mut rng,
        base: trace1,
        trace: Trace::default()
    },
    make_model()
);

// 3. Scoring - compute log-probability of trace2's choices
let (value3, score_trace) = runtime::handler::run(
    ScoreGivenTrace {
        base: trace2,
        trace: Trace::default()
    },
    make_model()
);
println!("Log-probability: {}", score_trace.total_log_weight());

§Type-Safe Effect Handling

The handler system preserves the natural return types of distributions:


let type_safe_model = prob! {
    // Each sample returns its natural type
    let is_outlier <- sample(addr!("outlier"), Bernoulli::new(0.1).unwrap());  // → bool
    let component <- sample(addr!("component"), Categorical::uniform(3).unwrap());  // → usize
    let count <- sample(addr!("events"), Poisson::new(3.0).unwrap());  // → u64
    let value <- sample(addr!("value"), Normal::new(0.0, 1.0).unwrap());  // → f64

    // Natural usage - no casting needed!
    let options = vec!["low", "medium", "high"];
    let strategy = options[component];  // Safe indexing with usize

    let multiplier = if is_outlier { 2.0 } else { 1.0 };  // Natural boolean logic
    let adjusted = value * multiplier + count as f64;  // Direct arithmetic

    pure((strategy, adjusted))
};

// Handler automatically dispatches to correct type-specific methods
let mut rng = StdRng::seed_from_u64(123);
let (_result, trace) = runtime::handler::run(
    PriorHandler { rng: &mut rng, trace: Trace::default() },
    type_safe_model
);

// Trace preserves type information
match &trace.choices[&addr!("outlier")].value {
    ChoiceValue::Bool(b) => println!("Outlier flag: {}", b),
    _ => unreachable!(),
}

§Custom Handler Implementation


/// Handler that logs all sampling operations
struct LoggingHandler<H: Handler> {
    inner: H,
    log: Vec<String>,
}

impl<H: Handler> Handler for LoggingHandler<H> {
    fn on_sample_f64(&mut self, addr: &Address, dist: &dyn Distribution<f64>) -> f64 {
        let value = self.inner.on_sample_f64(addr, dist);
        self.log.push(format!("Sampled {} = {:.3}", addr, value));
        value
    }

    fn on_sample_bool(&mut self, addr: &Address, dist: &dyn Distribution<bool>) -> bool {
        let value = self.inner.on_sample_bool(addr, dist);
        self.log.push(format!("Sampled {} = {}", addr, value));
        value
    }

    fn on_sample_u64(&mut self, addr: &Address, dist: &dyn Distribution<u64>) -> u64 {
        let value = self.inner.on_sample_u64(addr, dist);
        self.log.push(format!("Sampled {} = {}", addr, value));
        value
    }

    fn on_sample_usize(&mut self, addr: &Address, dist: &dyn Distribution<usize>) -> usize {
        let value = self.inner.on_sample_usize(addr, dist);
        self.log.push(format!("Sampled {} = {}", addr, value));
        value
    }

    fn on_observe_f64(&mut self, addr: &Address, dist: &dyn Distribution<f64>, value: f64) {
        self.log.push(format!("Observed {} = {:.3}", addr, value));
        self.inner.on_observe_f64(addr, dist, value);
    }

    fn on_observe_bool(&mut self, addr: &Address, dist: &dyn Distribution<bool>, value: bool) {
        self.log.push(format!("Observed {} = {}", addr, value));
        self.inner.on_observe_bool(addr, dist, value);
    }

    fn on_observe_u64(&mut self, addr: &Address, dist: &dyn Distribution<u64>, value: u64) {
        self.log.push(format!("Observed {} = {}", addr, value));
        self.inner.on_observe_u64(addr, dist, value);
    }

    fn on_observe_usize(&mut self, addr: &Address, dist: &dyn Distribution<usize>, value: usize) {
        self.log.push(format!("Observed {} = {}", addr, value));
        self.inner.on_observe_usize(addr, dist, value);
    }

    fn on_factor(&mut self, logw: f64) {
        self.log.push(format!("Factor: {:.3}", logw));
        self.inner.on_factor(logw);
    }

    fn finish(self) -> Trace {
        for entry in &self.log {
            println!("{}", entry);
        }
        self.inner.finish()
    }
}

// Example usage

§Design & Evolution

§Status

  • Stable: Core Handler trait and run function are stable since v0.1
  • Type-safe effects: The type-specific handler methods are a key architectural decision
  • Performance: Zero-cost abstraction - compiles to direct function calls

§Key Design Principles

  1. Separation of Concerns: Models describe what to compute, handlers define how to interpret
  2. Type Preservation: Each distribution type gets its own handler method to avoid lossy conversions
  3. Effect Isolation: All side effects (randomness, trace updates) are isolated in handlers
  4. Composability: Handlers can wrap other handlers for cross-cutting concerns

§Invariants

  • Handlers must be deterministic given the same inputs and RNG state
  • The finish() method is called exactly once at the end of execution
  • Type-specific methods must preserve the semantics of the distribution types
  • Trace updates must maintain internal consistency (addresses, log-weights)

§Proposal Workflow

Handler extensions follow the standard RFC process:

  1. Open a Design Proposal (DP) issue for new handler types
  2. Implement behind feature flag for experimental handlers
  3. Stabilize based on usage feedback and performance characteristics
  4. Document patterns and integration points

§Evolution Strategy

  • Backwards Compatible: New handler methods can be added without breaking existing implementations
  • Performance: Handler dispatch is compile-time resolved, enabling aggressive optimization
  • Extensibility: The trait design supports both simple and complex handler implementations

§Error Handling

Handlers must gracefully handle several error conditions:

§Distribution Parameter Errors

// Handlers should validate distribution parameters
fn on_sample_f64(&mut self, addr: &Address, dist: &dyn Distribution<f64>) -> f64 {
    let value = dist.sample(&mut rand::thread_rng());
    if !value.is_finite() {
        // Log error, return default, or propagate failure
        eprintln!("Invalid sample at {}: {}", addr, value);
        return 0.0; // or handle appropriately
    }
    value
}

§Address Collisions

  • The same address used twice in a model is a serious error
  • ReplayHandler and ScoreGivenTrace must handle missing addresses gracefully
  • Consider using Result<T, HandlerError> for handlers that can fail

§Trace Consistency

  • Log-weights must remain finite during normal operation
  • Infinite log-weights (from guard(false) or impossible observations) should be handled explicitly
  • Memory handlers must manage trace lifecycle correctly

§Best Practices

  • Always check is_finite() on log-probabilities and samples
  • Use defensive programming for address lookups in replay scenarios
  • Provide clear error messages that include the problematic address
  • Consider timeouts for handlers that might run indefinitely

§Integration Notes

§With Model System

  • Handlers work seamlessly with all Model variants and combinators
  • The run function handles Model continuation passing automatically
  • Type dispatch happens at compile time through the SampleType trait

§With Inference Algorithms

  • MCMC: Uses combinations of ReplayHandler and ScoreGivenTrace for proposals and acceptance
  • SMC: Uses PriorHandler for particle generation and ScoreGivenTrace for reweighting
  • ABC: Uses PriorHandler with custom distance functions in the handler logic

§Performance Characteristics

  • Handler dispatch is zero-cost (resolved at compile time)
  • Trace operations are O(log n) for address lookups using BTreeMap
  • Address clones are allocation-free (Arc<str> + cached hash), keeping per-site bookkeeping cheap
  • Type preservation avoids boxing/unboxing costs

§Core Types

  • Handler - Main handler trait definition
  • run - Model execution function
  • Trace - Execution trace representation
  • Model - Probabilistic model types

§Built-in Handlers

§Usage Patterns

§Examples

Traits§

Handler
Core trait for interpreting probabilistic model effects.

Functions§

run
Execute a probabilistic model using the provided handler.