Implementing a combination of exact and approximate methods

I am trying to implement a certain probability distribution. This distribution has an exact and an approximate form. Generally this goes well:

struct MyDistribution {
  approximation: Approximation,
}

enum Approximation {
  Existing(ExistingDistribution),
  Exact,
}

impl Continuous for MyDistribution {
  fn pdf(&self, x: f64) -> f64 {
    match self.approximation {
      Approximation::Existing(distribution) => distribution.pdf(x),
      Approximation::Exact => {
        // Some calculations
        todo!();
      }
    }
  }
}

However, in one of the trait implementations I want to use the default implementation for Approximation::Exact and a specific implementation from ExistingApproximation for Approximation::Existing. I don't think that is possible (implementing traits for variants) even though it should be fine if all the variants have an implementation. I could make a separate struct for the exact distribution and then wrap both in MyDistribution but that would duplicate a lot of code. Is there a better approach?

Can you give an example where the pattern you show here doesn't work? I'd normally just call the default implementation inside the Approximation::Exact match arm.

How would you call the default implementation? As far as I could tell, the following implementation of ContinuousCDF::inverse_cdf would recurse infinitely for SignedRank { approximation: Approximation::Exact }.

use statrs::distribution::{ContinuousCDF, Normal};

pub struct SignedRank {
    approximation: Approximation,
}

enum Approximation {
    Normal(Normal),
    Exact,
}

impl ContinuousCDF<f64, f64> for SignedRank {
    fn cdf(&self, x: f64) -> f64 {
        match self.approximation {
            Approximation::Normal(normal) => 2.0 * normal.cdf(x),
            Approximation::Exact => /* ... */ 0.0,
        }
    }

    fn inverse_cdf(&self, x: f64) -> f64 {
        match self.approximation {
            Approximation::Normal(normal) => normal.inverse_cdf(x),
            Approximation::Exact => ContinuousCDF::inverse_cdf(self, x),
        }
    }
}

I'm not familiar with the statsrs package, but I don't understand how this could work with any implementation: Where is the raw data coming from that is necessary to calculate the CDF in the Exact case?

I removed that for the example to keep it short, sorry. SignedRank has an additional field n: usize which is sufficient:

let r = x.ceil() as usize;

let mut sum = 1;

for n in 1..=self.n {
    let n_choose_2 = binomial(n as u64, 2) as usize;
    if n_choose_2 > r { continue }
    for i in n..=(r - n_choose_2) {
        sum += partitions(i, n, self.n - n + 1);
    }
}

sum as f64 / 2_f64.powi(self.n as i32 - 1)

Full file on GitHub

I see. In this case, I'd probably consider putting the enum at the top level:

enum SignedRank {
    Normal(Normal),
    Exact(ExactSignedRank)
}

struct ExactSignedRank(usize);

Then, you can defer to the ContinuousCDF implementation of the variant's argument in both cases.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.