Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make HasKernel public or introduce geo::Float trait? #577

Closed
michaelkirk opened this issue Dec 28, 2020 · 6 comments
Closed

Make HasKernel public or introduce geo::Float trait? #577

michaelkirk opened this issue Dec 28, 2020 · 6 comments

Comments

@michaelkirk
Copy link
Member

michaelkirk commented Dec 28, 2020

A lot of our algorithms depend on Float + HasKernel.

When downstream users want to implement methods that are generic across these same types they are stymied because HasKernel is not public outside of the geo crate.

We could make HasKernel public, and then they could implement for Float+HasKernel, but another option (and my current preference) would be to export a combined trait like:

pub trait geo::Float: geo::CoordinateType + geo::HasKenel + num_traits::Float { }

Note, I came across this issue while working on urschrei/polylabel-rs#6 which is a downstream user of geo.

public HasKernel approach looks like:

geo: https://github.com/georust/geo/compare/mkirk/pub-has-kernel?expand=1

downstream geo user (polylabel): https://github.com/urschrei/polylabel-rs/compare/master...michaelkirk:mkirk/update-geo?expand=1

geo::Float approach (my preference) looks like:

geo: https://github.com/georust/geo/compare/mkirk/geo-float?expand=1
downstream geo user (polylabel): https://github.com/urschrei/polylabel-rs/compare/master...michaelkirk:mkirk/update-geo-2?expand=1

The reason I prefer the geo::Float approach is:

  1. it's a bit tidier from the client perspective (rather than num_traits::Float+geo::HasKernel they just have geo::Float (in fact we could tidy up the geo code base a bit with this)

  2. The HasKernel type is a bit unintuitive for a casual reader. I think the concept of a Float will be more approachable for people just "trying to make things work".

It's very possible there's a better approach, or that I'm missing something in my analysis, so curious what other folks think.

@urschrei
Copy link
Member

I'm in favour of the new public geo::Float trait approach. We can clearly and succinctly document what it's for and what it does in a few lines, and spare downstream users the (entirely necessary internal) complexity of HasKernel IMO. @rmanoka what do you think?

@rmanoka
Copy link
Contributor

rmanoka commented Dec 29, 2020

TL;DR: I suggest we do both: create a "alias" trait geo::Float, as well as make HasKernel public. Actually just making the alias trait public, would still be unusable as the end-user would anyway need to implement HasKernel to implement the alias trait on a new type.

Comp. geom. algorithms involve two concepts: constructions (such as finding mid point of a line segment), and predicate (such as checking if point lies on / above / below a line segment). In general, constructions are not exact unless we use very complicated exact-arithmetic types (eg. mid point of a line-segment with any finite precision coords. like i64, f64 is inexact due to the finite precision). Otoh, predicates can be exact even for finite precision coords. and in fact the Shewchuk robust predicates do exactly that for f64.

Our HasKernel trait is meant to abstract out the robust predicates, so we can provide implementations of the algos. for integer, float, and possibly other types. The Float trait is used where we need constructions, and thus may be in-exact. So, it seems useful to make HasKernel public, as well as introduce new geo::Float as a synonym for HasKernel + num::Float. Note that HasKernel actually has default implementations of all it's methods, so the user can get away with not implementing anything if they do not care about robustness.

The technicality of HasKernel is unfortunate, but I don't know how to circumvent it if we want to support robust predicates for generic scalar types. In fact, other comp. geom. libs like spade, and CGAL also use this concept of a "Kernel" (and was the inspiration for our impl. too). Our lib. has a key advantage over JTS/geos in that it supports generic scalar types, while also aiming to be robust and conform to standards such as SFS. IMO, we should keep it that way.

I do agree that we should improve the docs for HasKernel, and may be provide an implementation guide on new types. Should we track that as a new issue? I could take a shot at it early next year :)

@michaelkirk
Copy link
Member Author

just making the alias trait public, would still be unusable as the end-user would anyway need to implement HasKernel to implement the alias trait on a new type

That's a good point! Though making a geo::Float alias trait public is sufficient for my purposes here: urschrei/polylabel-rs#6, it wouldn't be sufficient for someone wanting to use types other than those we've already has_kernel!'d.

The technicality of HasKernel is unfortunate

To be clear, I think the HasKernel stuff makes a lot of sense as is, and I'm glad you did it that way.

Only based on the name, I'm hoping non-experts will have a better intuition that "The thing you're probably looking for is geo::Float" rather than "The thing you're probably looking for is geo::HasKernel + num_traits::Float"

At the point people want to do geometry with exotic numeric types, then it's more reasonable for us to expect them to understand exactly what the geo::HasKernel vocabulary is for, until then, the 99% of people who are just doing stuff like: urschrei/polylabel-rs#6 will see that we have our own weird Float trait and that they need to use it if they want to be generic.

So, to recap, I've been persuaded by @rmanoka that we should both add a public geo::Float trait and also make geo::HasKernel public.

@michaelkirk
Copy link
Member Author

michaelkirk commented Dec 29, 2020

I do agree that we should improve the docs for HasKernel, and may be provide an implementation guide on new types. Should we track that as a new issue? I could take a shot at it early next year :)

To address this directly and reiterate: my concern was not that HasKernel was unnecessarily complicated (though better docs are always welcome!), but that I'm hoping to avoid exposing people to it directly unless they are doing stuff with exotic number types.

@urschrei
Copy link
Member

To address this directly and reiterate: my concern was not that HasKernel was unnecessarily complicated

I think this was my doing, and just to reiterate what @michaelkirk said: the HasKernel mechanism is an excellent design and I'm delighted with its implementation! The fact that it's complex is inherent to its intended aim, and I think I speak for everyone when I say that it's solved the problem very well.

bors bot added a commit that referenced this issue Jan 1, 2021
583: Allow external crates to implement generic float methods for Geometry<T> r=rmanoka,urschrei a=michaelkirk

- [x] I agree to follow the project's [code of conduct](https://github.com/georust/geo/blob/master/CODE_OF_CONDUCT.md).
- [x] I added an entry to `CHANGES.md` if knowledge of this change could be valuable to users.
---

Solves #577 (see discussion there for context)

You can see it in use here: https://github.com/urschrei/polylabel-rs/pull/6/files


Co-authored-by: Michael Kirk <[email protected]>
@michaelkirk
Copy link
Member Author

This was done in #583

bors bot added a commit that referenced this issue Jan 13, 2021
602: +Debug for CoordinateType r=frewsxcv a=michaelkirk

- [x] I agree to follow the project's [code of conduct](https://github.com/georust/geo/blob/master/CODE_OF_CONDUCT.md).
- [TODO] I added an entry to `CHANGES.md` if knowledge of this change could be valuable to users.
---

This is intended to address #597

I took a broader approach, and wonder if people are interested in it. The tldr is:

### geo-types
- deprecate `CoordinateType`; (now includes `std::fmt::Debug`)
- Add `trait CoordNum: CoordinateType;` 
- Add `trait CoordFloat: CoordNum + num_traits::Float`

### geo
Add `trait GeoNum: CoordNum + HasKernel`
Add `trait GeoFloat: CoordFloat + HasKernel`

**downstream adoption**

If your crate interops with geo-types, you may need  to make changes like this:

```
pub fn foo<T>(line_string: &geo_types::LineString<T>)
where
-    T: num_traits::Float,
+    T: geo_types::CoordFloat,
{
```

or this (this one is actually only a deprecation, so you could leave it as is for now, but eventually you'll need to make the change):
```
pub fn foo<T>(line_string: &geo_types::LineString<T>)
where
-    T: geo_types::CoordinateType
+    T: geo_types::CoordNum
{
```

**alternative considered**

If I went with the minimal approach to adding Debug trait, changes would instead need to look like this:

This one, for non-floats, could remain untouched:
```
pub fn foo<T>(line_string: &geo_types::LineString<T>)
where
    T: geo_types::CoordinateType
{
```

But anything restricted to floats would need to explicitly include Debug

```
pub fn foo<T>(line_string: &geo_types::LineString<T>)
where
-    T: num_traits::Float,
+    T: num_traits::Float + std::fmt::Debug,
{
```

**motivation:**

I initially completed #597 with the most conservative approach, achieved in the first two commits. The first simply adds Debug to CoordinateType and the second fixes the resulting compiler errors. 

This is a breaking change for anyone constraining generic implementations to just `num_traits::Float`, e.g. like [geojson crate does](https://github.com/georust/geojson/blob/master/src/conversion/to_geo_types.rs#L31) (fix here: https://github.com/georust/geojson/compare/mkirk/geo-types-0.7?expand=1), and polylabel (fix here: https://github.com/urschrei/polylabel-rs/compare/master...michaelkirk:mkirk/geo-types-0.7?expand=1)

Compare this to a crate like [geo-booleanop](https://github.com/21re/rust-geo-booleanop/blob/master/lib/src/boolean/helper.rs#L61), for which things "just work" because they targeted CoordinateType.

This got me thinking that, similar to #577 (yet unreleased), if we want people to write code targeting geo-types in a generic way, it might be helpful to make it clearer and easier how to do so.

So, I've added a CoordFloat and a CoordNum which is basically just a rename of CoordinateType, and applied them liberally within our own code base. I'm hoping this would drive others to reach for these traits rather than specific num_traits when implementing code intended for generic geo-types interop. 

For crates like geo-booleanop, which targeted CoordinateType, things "just work" after the update, but they'll see a deprecation notice like:
```
$ cargo build
warning: use of deprecated trait `geo_types::CoordinateType`: use `CoordFloat` or `CoordNum` instead
 --> lib/src/boolean/helper.rs:2:29
  |
2 | use geo_types::{Coordinate, CoordinateType};
  |                             ^^^^^^^^^^^^^^
  |
  = note: `#[warn(deprecated)]` on by default
```

Which I think is acceptably clear.

It's a large number of lines changed change overall, I know! But it's mostly mechanical, and I thought, since we're breaking geo-types, maybe this is a good time to bite the bullet and avoid some future pain. 

If it's too controversial, as I said, the minimal amount of change we need to enable Debug for CoordinateType is the first three commits (through and including "address fallout of "Add Debug to CoordinateType"). It's still a pretty big change with just that, and note that this will still break the crates I mentioned. I intend to fix the ones I know about, but there's a long tail of crates, which is why I hate breaking geo-types. 

In the few that I've checked it's been quick to adapt to the change, but it might be worth announcing and letting sit open for a while in case downstream geo-types users want to weigh in.

Co-authored-by: Michael Kirk <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants