cgmath icon indicating copy to clipboard operation
cgmath copied to clipboard

Fine grained trait bounds

Open elrnv opened this issue 4 years ago • 8 comments

This is possibly more of a question. Would it be of any interest to the users of cgmath to have more fine grained trait bounds on methods from std::ops to eliminate the need to add BaseNum or BaseFloat trait bounds into generic code in libraries that use cgmath? The motivation here is that it may perhaps be easier to swap in/out cgmath in libraries without modifying the trait bounds on all generic code. I know this may be a moot point at this moment, but seeing as cgmath aims to be one of the simpler and more light-weight matrix libs this might make sense here.

elrnv avatar Nov 05 '19 19:11 elrnv

~~Another benefit of this is that libraries that implement lower level types like a custom floating point type (e.g. auto-diff, complex) would not need to implement BaseFloat, but will need to only target num_traits.~~

Edit: NVM about this, just realized this is handled automatically by the blanket impl of BaseFloat.

elrnv avatar Nov 05 '19 21:11 elrnv

I think an example would be useful here. What kind of stuff you'd be fine graining the trait bounds for?

kvark avatar Nov 19 '19 20:11 kvark

Sure! Suppose I have bunch of code that looks like:

use num_traits::Zero;
fn make_some_mtx<T: Zero + Copy>() -> [[T;2];2] {
    let mtx = [[T::zero(); 2]; 2];
    // Maybe do something else with mtx.
    mtx
}

and for whatever reason I am using arrays, but now i want to use cgmath, so I write:

use num_traits::Zero;
use cgmath::Matrix2;
fn make_some_mtx<T: Zero + Copy>() -> Matrix2<T> {
    let mtx = Matrix::zero();
    // Maybe do something else with mtx.
    mtx
}

Which wont work since Matrix::zero() requires all of BaseFloat:

impl<S: BaseFloat> Zero for Matrix2<S> {
    #[inline]
    fn zero() -> Matrix2<S> {
        #[cfg_attr(rustfmt, rustfmt_skip)]
        Matrix2::new(
            S::zero(), S::zero(),
            S::zero(), S::zero(),
        )
    }

    #[inline]
    fn is_zero(&self) -> bool {
        ulps_eq!(self, &Self::zero())
    }
}

This means I have to also change my bounds:

use num_traits::Zero;
use cgmath::{BaseFloat, Matrix2};
fn make_some_mtx<T: BaseFloat>() -> Matrix2<T> {
    let mtx = Matrix::zero();
    // Maybe do something else with mtx.
    mtx
}

So for this particular example, the win isn't too much since I need the bound for ulps_eq as well, but there are other examples like SquareMatrix that require BaseFloat, although from_value or from_diagonal really should only need Zero and Copy.

(I personally would suggest a further step and have is_zero just do PartialEq with Zero, and have a separate is_approx_zero fn for measuring relative equivalence (in my personal experience float comparisons tend to be domain specific).)

But even having:

impl<S: Zero + Copy + UlpsEq> Zero for Matrix2<S> { ... }

would be nice. Again I understand this is opinionated since having a single trait define most scalars has its benefits, which is why it's more of a question.

elrnv avatar Nov 21 '19 01:11 elrnv

Thank you for providing the detailed examples! I agree, this would be a welcome feature.

kvark avatar Nov 21 '19 02:11 kvark

Would this make it easier to use fixed precision numbers with cgmath? Having square root would be enough to get stuff like magnitude and distances. I tried implementing enough of Float for the fixed crate and it seemed to work reasonably well, but there are some trait methods of Float that don't make sense for fixed precision numbers: https://gitlab.com/spearman/fixed/blob/8ed9f22ae221bbd7b1b8eb0c1ac8e31159b35c94/src/num.rs#L179-404

spearman avatar Nov 26 '19 06:11 spearman

I think breaking down the trait bounds may help using types not already implementing all of BaseFloat, but I wasn’t intending to break apart num_traits::Float itself. I think that would be an interesting proposal for num_traits.

elrnv avatar Nov 27 '19 18:11 elrnv

In this context, can/should cgmath::BaseNum and cgmath::BaseFloat be replaced with num_traits::PrimInt and num_traits::Float? Perhaps num_traits::PrimInt has too many restrictions and a trait representing a field would be more flexible? Taking the granularity to std::ops::Add for each function may make cgmath harder to use and changing performed computations may require adding or removing traits causing breaking changes.

I think we need to collect concrete examples like those provided by @spearman and @elrnv to figure out how to actually improve the situation. Also someone with a good understanding of the mathematical categorization can help grouping and naming behaviour (traits).

mickvangelderen avatar Feb 15 '20 13:02 mickvangelderen

I think not entirely since BaseFloat and BaseNum have additional useful traits from approx, but probably many bounds don’t need those.

elrnv avatar Feb 16 '20 15:02 elrnv