Closed RazrFalcon closed 3 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
.
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:
NormalizedValue<T>
- 0..1 f32/f64NonZero<T>
- any number except zero i8/i16/i32/i64/f32/f64. Useful for cases, when value used as a divider, to prevent n/0
.Negative<T>
- <0 i8/i16/i32/i64/f32/f64Positive<T>
- >0 u8/u16/u32/u64/f32/f64, basically unsigned integers and floats, but never zero. Name a bit confusing. Maybe NonZeroPositive
Range<T, T>
- a number with specific bounds. Could be confused with std::ops::Range
, so maybe a different name is required.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
}
}
I have an uneasy feeling about this. Here are the concerns:
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>
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.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.
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?
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.
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.
Out of scope I guess.
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?