Design of polymorphic adaptor for ndhistogram

ndhistogram provides a generic Histogram type, with, among others, fill and value methods:

pub trait Histogram<A: Axis, V> {
  // ...
  fn value(&self, coordinate: &A::Coordinate) -> Option<&V> { ... }
  fn fill(&mut self, coordinate: &A::Coordinate)
  where
      V: Fill,
  { ... }
  // ...
}

These histograms can have arbitrary dimensions, combining axes with a variety of possible features. For example

  • A 1D histogram with an axis with uniformly-spaced bins and overflow and underflow bins:

    ndgistogram!(Uniform::new(nbins, low, high); usize)
    
  • A 2D histogram with no over/underflow bins on the second axis

    ndhistogram!(
        Uniform      ::new(nbins1, low1, high1),
        UniformNoFlow::new(nbins2, low2, high2);
        usize)
    

I would like to create an adaptor which allows the client to use the aforementioned value and fill methods of Histogram in terms of some TypeICareAbout, rather than the specific choice of axes. When creating/instantiating/defining a specific adaptor, the client would do two things:

  1. Specify the space covered by the histogram. In other words, a choice of axes to be given to ndhistogram!.

  2. Provide a mapping from some TypeICareAbout to the coordinates that need to be passed to the fill and value methods, for that choice of axes. Very roughly,

    fn to_coordinate(t: &TypeICareAbout) -> <???>::Coordinate 
    

The mapping should be used in the adaptor's fill and value methods, roughly like this

fn fill (&mut self, t: &TypeICareAbout)          {  self.histogram.fill (&to_coordinate(t)); }
fn value(&    self, t: &TypeICareAbout) -> usize { *self.histogram.value(&to_coordinate(t)).unwrap_or(&0) }

Enabling the client to interact with the underlying histogram in terms of TypeICareAbout, without caring about the specific axes being used, beyond the initial choice of axes and mapping function.

How can this be expressed in Rust?

I ended up going with

pub struct MappedAxis<T,A>
where
    A: Axis,
{
    axis: A,
    map: Box<dyn Fn(&T) -> A::Coordinate>,
}

whose purpose is to specify a type of axis along with a mapping function from T (aka TypeICareAbout in the OP) to the Axis' Coordinate.

This can be turned into an Axis itself by implementing the trait:

impl<T,A> Axis for MappedAxis<T,A>
where
    A: Axis,
{
    type Coordinate = T;

    type BinInterval = A::BinInterval;

    fn index(&self, coordinate: &Self::Coordinate) -> Option<usize> {
        self.axis.index(&(self.map)(coordinate))
    }

    fn num_bins(&self) -> usize {
        self.axis.num_bins()
    }

    fn bin(&self, index: usize) -> Option<Self::BinInterval> {
        self.axis.bin(index)
    }
}

Then there's a MappedHistogram trait with the methods required in the OP

type Tica = TypeICareAbout;

pub trait MappedHistogram {
    fn fill (&mut self, x: &Tica);
    fn value(&    self, x: &Tica) -> usize;
}

which needs to be implemented separately for every distinct dimensionality of histogram that is supported, with a lot of boilerplate

impl<X> MappedHistogram for ndhistogram::Hist1D<X, usize>
where
    X: Axis<Coordinate = Tica>,
{
    fn fill (&mut self, x: &Tica)          {  Histogram::fill (self, x) }
    fn value(&    self, x: &Tica) -> usize { *Histogram::value(self, x).unwrap_or(&0) }
}

impl<X, Y> MappedHistogram for ndhistogram::Hist2D<X, Y, usize>
where
    X: Axis<Coordinate = Tica>,
    Y: Axis<Coordinate = Tica>,
{
    fn fill (&mut self, x: &Tica)          {  Histogram::fill (self, &(*x, *x)) }
    fn value(&    self, x: &Tica) -> usize { *Histogram::value(self, &(*x, *x)).unwrap_or(&0) }
}

impl<X, Y, Z> MappedHistogram for ndhistogram::Hist3D<X, Y, Z, usize>
where
    X: Axis<Coordinate = Tica>,
    Y: Axis<Coordinate = Tica>,
    Z: Axis<Coordinate = Tica>,
{
    fn fill (&mut self, x: &Tica)          {  Histogram::fill (self, &(*x, *x, *x)) }
    fn value(&    self, x: &Tica) -> usize { *Histogram::value(self, &(*x, *x, *x)).unwrap_or(&0) }
}

// etc ...

the key point being that the single argument to fill/value of TypeICareAbout needs to be passed in as N separate arguments of each of the N underlying axes.

(Tica (aka TypeICareAbout) could/should be a type parameter, but it's not needed for my specific use case.)

It's probably not the most concise, or efficent, or sensible ... but it lets me get on with exploring the specific behaviours behind the interface, for now.