kvark / mint

Math Interoperability Types
MIT License
256 stars 20 forks source link

Generic numeric types? #52

Closed RazrFalcon closed 3 years ago

RazrFalcon commented 4 years ago

Does numeric wrappers like normalized value (always in 0..1 range), only negative, only positive, non-zero, arbitrary range are in scope of this crate?

kvark commented 4 years ago

@RazrFalcon great question! I haven't used type-enforced wrappers for this yet, but I see how this is useful. More often than not, you'd want these guarantees to be enforced at the library/API barrier, so this should be in scope for mint.

RazrFalcon commented 4 years ago

Nice! I'm already using simple wrappers in my projects, but I wanted a more generic solution.

I'm also willing to work on a patch. The main question is API. Right now I'm using immutable wrappers, so it simplifies thing a lot, but people may prefer mutable one too.

The types I expect to have are:

The simpliest implementation is:

// Internals are private. 
// Should be T eventually.
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct NonZero(f64);

impl NonZero {
    pub fn new(n: f64) -> Option<Self> {
        if n != 0.0 { // `float-cmp` should be used here
            None
        } else {
            Some(NonZero(n))
        }
    }

    pub fn value(&self) -> f64 {
        self.0
    }
}

impl std::ops::Deref for NonZero {
    type Target = f64;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
kvark commented 4 years ago

I have an uneasy feeling about this. Here are the concerns:

  1. mint is designed to not have any logic, it's API only. Any kind of restrictions checking, transformation, etc, should be done elsewhere. This means we can't have NonZero::new(T) -> Option<Self>
  2. as a consequence, mint is fully open: all fields are public, easy to construct by hand. It's meant to communicate the contract, but not enforce it. Enforcing properties is a big topic that can't be split off general transformations, i.e. enforcing that a matrix is orthonormal requires the dot product.
  3. it's not clear what boundary to draw, i.e. one app may require X != 0.0 but another one would want abs(X) > epsilon for nearly the same kind of API. Along this line, Range<X,Y> would have to be split into: including/non-including from both sides, bound/unbound from both sides, which doesn't sound simple enough, and would unlikely work well for everybody.

Saying this, I think there is still room for some type-level semantics like UnitVector<T> that is provided without enforcement. We could make it open (all fields public) and move the validation/checks to the code that converts the types of a specific library (e.g. cgmath) into this. This way, if a library already has this semantics exposed, we'd not need to do any run-time checks. This also means that the code on the receiving end (typically, engine code) would still need to assert that the guarantees are met before processing the values.

RazrFalcon commented 4 years ago

I see. Makes sense. Originally, I thought about creating a new crate, but I wanted to check existing crates first. mint looked like the closest one. Maybe you are familiar with crates that implement something like this?

kvark commented 4 years ago

IIRC, nalgebra has some of these, but that's the extend of my knowledge :) I'd love to use some if they are available generically over match libraries.

Ralith commented 4 years ago

nalgebra has an abstract struct Unit<T> for vectors and quaternions (and maybe other norm-having things). It's handy, but correct operations on it can eventually cause normalization to be lost due to rounding, so it's not a rock-solid guarantee.

I'm concerned that adding stuff like this to mint opens the door to arbitrarily large API expansion, which is a major risk for a crate whose value rests entirely on not having breaking changes.

RazrFalcon commented 3 years ago

Out of scope I guess.