Bourgeois enums are fortunate enough to own, while WeNums can only borrow

I apologize for the title. My question is not quick, so I thought I should dress it up.

I'm looking for inspiration / feedback / vocabulary on an approach I'm taking related to enums whose associated data are owned types vs borrowed types.

disclaimer: example code has been changed to protect the innocent.

The code I'm working on is for the geo crate, which is a collection of geospatial algorithms and data types. My examples below references some of the actual geospatial type names for the sake of concreteness only - their geospatial functionality isn't relevant. It's also been edited to better stand alone as an example.

Background

The primary currency of the geo crate is the Geometry enum, for which each variant is an owned type of the same name (is there a conventional name for this pattern? "OneOf the bourgeoisie"?)

pub enum Geometry<T>
where
    T: GeoNum,
{
    Point(Point<T>),
    Line(Line<T>),
    LineString(LineString<T>),
    Polygon(Polygon<T>),
    MultiPoint(MultiPoint<T>),
    MultiLineString(MultiLineString<T>),
    MultiPolygon(MultiPolygon<T>),
    GeometryCollection(GeometryCollection<T>),
    Rect(Rect<T>),
    Triangle(Triangle<T>),
}

I'm working on a feature for which I've added a reference corollary to the crate like this:

pub enum GeometryRef<'a, T>
where
    T: GeoNum,
{
    Point(&'a Point<T>),
    Line(&'a Line<T>),
    LineString(&'a LineString<T>),
    Polygon(&'a Polygon<T>),
    MultiPoint(&'a MultiPoint<T>),
    MultiLineString(&'a MultiLineString<T>),
    MultiPolygon(&'a MultiPolygon<T>),
    GeometryCollection(&'a GeometryCollection<T>),
    Rect(&'a Rect<T>),
    Triangle(&'a Triangle<T>),
}

The back story is that, in the crates history, most functionality has been added via trait impls for individual Geometry variants.

For example consider this trait and it's many impls:

pub trait Contains<Rhs = Self> {
    /// Returns whether `rhs` is entirely within `self`
    /// ```
    /// assert!(usa_polygon.contains(center_of_chicago_point))
    /// assert!(!center_of_chicago_point.contains(usa_polygon))
    fn contains(&self, rhs: &Rhs) -> bool;
}

impl<T: GeoNum> Contains<Point<T>> for Point<T>
{
    fn contains(&self, p: &Point<T>) -> bool {
        self.0 == &p.0
    }
}

impl<G, T> Contains<G> for MultiPoint<T>
where
    T: GeoNum,
    Point<T>: Contains<G>,
{
    fn contains(&self, rhs: &G) -> bool {
        self.iter().any(|p| p.contains(rhs))
    }
}

impl<T: GeoNum> Contains<Point<T>> for Line<T>
{
    fn contains(&self, p: &Point<T>) -> bool {
        // and so on
    }
}

impl<T: GeoNum> Contains<Line<T>> for Line<T>
{
    fn contains(&self, line: &Line<T>) -> bool {
        // and so on
    }
}

// ... and so on, with an `impl Contains<B> for A`  for every combination of Geometry cases

Some traits are not implemented for every Geometry variant. e.g. a trait that measures "length" might make sense for a Line but not for a Polygon. But for traits like Contains above, which are implemented for every Geometry variant, we typically impl that trait on Geometry itself by way of a trivial delegation:

impl<T: GeoNum> Contains for Geometry<T> {
    fn contains(&self) -> bool {
        match self {
            Geometry::Point(g) => g.contains(),
            Geometry::Line(g) => g.contains(),
            Geometry::LineString(g) => g.contains(),
            ... => etc.
        }
    }
}

(Since this delegation is common boilerplate, we use a macro for the actual implementation)

I'm currently adding some functionality via a new Relate trait which, rather than having a bottom-up impl for every combination of variants, has a single implementation directly on Geometry. Like this:

impl<T: GeoFloat> Relate<T, Geometry<T>> for Geometry<T> {
    fn relate(&self, other: &Geometry<T>) -> IntersectionMatrix {
       // This needs a read-only borrow of the two `Geometry`
       topologically_relate(self, other)

       // ...not relevant to the Rust question, but for context, the above outputs topological 
       // relations of the two geometries as described by https://en.wikipedia.org/wiki/DE-9IM
    }
}

So I had the above trait, implemented on Geometry and it's working great. BUT sometimes I'd like to use this trait on instances of the individual inner variant (Point, Line, etc.), but the only way to do that, would require some undesirable cloning:

fn foo(line: &Line, polygon: &Polygon) -> Color {
    # 🚨We want to avoid these clones!
    let line_geometry = Geometry::Line(line.clone());
    let polygon_geometry = Geometry::Polygon(polygon.clone());

    if polygon_geometry.relate(line_geometry).is_contains() {
        Color::Green
    } else {
        Color::Red
    }
}

So I addressed this by introducing a new GeometryRef wenum (as described above), which is just like the Geometry enum, but each variant holds a reference, rather than an owned.

pub enum GeometryRef<'a, T>
where
    T: GeoNum,
{
    Point(&'a Point<T>),
    Line(&'a Line<T>),
    LineString(&'a LineString<T>),
   ...
}

And reimplemented Relate and it's inner workings in terms of GeometryRef like:

impl<T: GeoFloat> Relate<F, GeometryRef<F>> for GeometryRef<F> {
    fn relate(&self, other: &GeometryRef <F>) -> IntersectionMatrix {
        // Also re-wrote the internals of `topologically_relate` to use `&GeometryRef`s rather than a `&Geometry`s
        // which was mostly straight forward
       topologically_relate(self, other)
    }
}

Then the problematic clones go away:

fn foo(line: &Line, polygon: &Polygon) -> Color {
    // 🥳 no more clones 👯‍♂️
    let line_geometry = GeometryRef::Line(line);
    let polygon_geometry = GeometryRef::Polygon(polygon);

    if polygon_geometry.relate(line_geometry).is_contains() {
        Color::Green
    } else {
        Color::Red
    }
}

So, that works! But here are the downsides:

The implementation of topologically_relate internally relies on a bunch of functionality that's currently only implemented for &Geometry.

So for now I've duplicated all that functionality for GeometryRef. So far it's been trivial to duplicate — whereas before we had something like this:

impl<T> BoundingRect<T> for Geometry<T>
where
    T: CoordNum,
{
    type Output = Option<Rect<T>>;

    fn bounding_rect(&self) -> Self::Output {
        match self {
            Geometry::Point(g) => Some(g.bounding_rect()),
            Geometry::Line(g) => Some(g.bounding_rect()),
            Geometry::LineString(g) => g.bounding_rect(),
            Geometry::Polygon(g) => g.bounding_rect(),
            Geometry::MultiPoint(g) => g.bounding_rect(),
            Geometry::MultiLineString(g) => g.bounding_rect(),
            Geometry::MultiPolygon(g) => g.bounding_rect(),
            Geometry::GeometryCollection(g) => g.bounding_rect(),
            Geometry::Rect(g) => Some(g.bounding_rect()),
            Geometry::Triangle(g) => Some(g.bounding_rect()),
        }
    }
}

We now also need an almost identical impl like this:

impl<T> BoundingRect<T> for GeometryRef<'_, T>
where
    T: CoordNum,
{
    type Output = Option<Rect<T>>;

    fn bounding_rect(&self) -> Self::Output {
        match self {
            GeometryRef::Point(g) => Some(g.bounding_rect()),
            GeometryRef::Line(g) => Some(g.bounding_rect()),
            GeometryRef::LineString(g) => g.bounding_rect(),
            GeometryRef::Polygon(g) => g.bounding_rect(),
            GeometryRef::MultiPoint(g) => g.bounding_rect(),
            GeometryRef::MultiLineString(g) => g.bounding_rect(),
            GeometryRef::MultiPolygon(g) => g.bounding_rect(),
            GeometryRef::GeometryCollection(g) => g.bounding_rect(),
            GeometryRef::Rect(g) => Some(g.bounding_rect()),
            GeometryRef::Triangle(g) => Some(g.bounding_rect()),
        }
    }
}

Finally, the question

This is doable... and can be cleaned up with a macro, but is there some better design that avoids all this duplication?

Or anyone have a similar experience? Or examples to point to? Horror stories? Sonnets?

One piece of advice I was given was that, if GeometryRef is going to become public, it might make sense to future-proof and use a Cow rather than a plain reference, since a Cow also solves the problem and might enable some future functionality without requiring introducing yet-another API change down the road. Something like:

pub enum GeometryCow<'a, T>
where
    T: CoordNum,
{
    Point(Cow<'a, Point<T>>),
    Line(Cow<'a, Line<T>>),
    LineString(Cow<'a, LineString<T>>),
    Polygon(Cow<'a, Polygon<T>>),
    MultiPoint(Cow<'a, MultiPoint<T>>),
    MultiLineString(Cow<'a, MultiLineString<T>>),
    MultiPolygon(Cow<'a, MultiPolygon<T>>),
    GeometryCollection(Cow<'a, GeometryCollection<T>>),
    Rect(Cow<'a, Rect<T>>),
    Triangle(Cow<'a, Triangle<T>>),
}

Thank you for reading this far. :heart:

4 Likes

No comment on which approach might be best, but you could have an AsRef-like trait to be generic over: Playground