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

Implement ops for primitives + Ratios; ops for BigRational + primitives #38

Open
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

turboladen
Copy link

Before this I was not able to use a primitive as the left-hand side of a core::ops::{Add, Sub, Mul, Div} method where the right-hand side was a Ratio of some type; this implements those. Also, I was not able to use a BigRational on the LHS and a primitive on the RHS of a core::ops::{Add, Sub, Mul, Div} method; this implements those as well (although I'm not sure I'm doing it in the most efficient code). I'm hoping these changes are welcome; I'm pretty new to the num crates and feel like I may have been missing something that should've been obvious.

Initially I'd manually implemented most of this stuff (not using generics), leading me to write a bunch of tests for all of the combinations of types, then refactored to use generics. Thus there's a bunch of tests that should cover all of the cases (hopefully not too many!). In doing so, it really started to feel like maybe some of the ops code could live in a separate file (mainly due to all of the tests), but since I'm not too familiar with this repo I decided to not rock the boat too much.

I've also done very little Rust work with others, so I may be missing some conventions--sorry about that! Please do let me know so I can tidy up accordingly.

These changes let you use a primitive on the LHS of an op statement
where the RHS is a `Ratio`. This also lets you use primitives on the RHS
of a `BigRational` equation.
@turboladen
Copy link
Author

Ok, I've got all builds except for the 1.15.0 one passing; I'm not sure how to deal with those {i,u}128 failures though. Any tips would be quite helpful.

@cuviper
Copy link
Member

cuviper commented Aug 21, 2018

When guarding code, please use #[cfg(has_i128)] instead of by feature, so it's still enabled by the build-script detection. For integer literals like 0u128, even guarded code becomes a problem since it still has to parse by the old compiler. You can let type inference handle it in some cases, like if calling fn foo(x: u128) then foo(0) is fine. In other cases you might have to assign a local first, let x: u128 = 0, which still parses on the old compiler even though it doesn't know that type.

@turboladen
Copy link
Author

Ah, thank you @cuviper, that was super helpful (and makes total sense). Fixed all of those.

The other failures seem to have to do with isize::from() not being implemented for u8 and i16 until 1.26.0. I'm not really sure how to fix those tests (they seem relevant for current versions) so I've just commented them out (don't really like doing that) and left a TODO. I can remove these or whatever seems preferable, just let me know.

Since `BigRational`s/`BigInt`s don't implement `Copy`, seems like it'd be nice to be able to pass a reference on `100 / &some_big_rational`.
Similar to the previous commit, this extends behavior of letting Ratios operate on primitives (and BigInt). In the case of `BigRational`, I imagine references to them will be more prevalent than the other types, hence this commit.
…ational/&BigRational

Also refactored related tests to use macros for defining inner test functions.
@turboladen turboladen force-pushed the feature/implement_ops_for_primitives branch from 44c61da to d1c392e Compare August 26, 2018 23:45
@turboladen
Copy link
Author

Are there any problems with this PR? Just wondering as I have some other changes I'm curious about making, but they sort of build on the changes here and I don't want to get too far along there if there are problems here. TIA

@cuviper
Copy link
Member

cuviper commented Sep 1, 2018

No problems AFAIK -- I was just on vacation and haven't come back to look at this again. I'll try to review it soon!

@turboladen
Copy link
Author

@cuviper oh, nice! I mostly just wanted to give it a bump so it doesn't die. Thanks!

Copy link
Member

@cuviper cuviper left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First off, I'm sorry that I took so long to properly review this!

(Ratio<T> for $primitive:ident) => {
impl<T> Add<Ratio<T>> for $primitive
where
T: Clone + Integer + ::core::convert::From<$primitive>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think adding a blanket impl<T> may be a breaking change, e.g. if someone already had their own impl Add<Ratio<Foo>> for i32. Honestly, I still get confused about exactly what the coherence rules would allow, but I think this would be legal for someone else to do, and your change would break it.

But regardless, I don't think we want implicit conversion between primitives. Note that the op implementations in the standard library don't do this conversion either. Idiomatic Rust is that primitive types should be explicitly converted as needed.

On the other hand, BigInt and BigUint do implement ops for the primitive types, and this is for performance reasons. They can entirely avoid allocating a new bigint buffer in most cases. However, here you defeat that idea if you use T::from here, because that forces a new allocation for that bigint T.

So I think the implementations we do want are:

  • impl Add<Ratio<$primitive>> for $primitive
  • impl Add<BigRational> for $primitive
  • impl Add<BigRational> for BigInt
  • could do Ratio<BigUint> too, but we're probably lacking in a lot of places for that...
  • & variations of all of these -- ref+value, value+ref, ref+ref


#[inline]
fn add(self, rhs: Ratio<T>) -> Ratio<T> {
Ratio::new(rhs.numer + T::from(self), rhs.denom)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As noted above, you lose the bigint performance win if you force T::from here. But this implementation is also incorrect. Look at the arith_impl! macro -- when adding an integer to a ratio, you need to multiply it by the denominator! e.g. your code would compute 1/3 + 1 = 2/3, but it should be 4/3.


#[inline]
fn sub(self, rhs: Ratio<T>) -> Ratio<T> {
Ratio::new(T::from(self) - rhs.numer, rhs.denom)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to Add, we don't want T::from, and the math is incorrect.

impl_primitive_ops_ratio!(Ratio<T> for BigInt);

#[cfg(feature = "bigint")]
macro_rules! impl_ops_bigrational_for_float {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I don't think it's a good idea to make float-conversions implicit, especially since you have to output an Option. That makes it pretty unusual from what most people would expect from ops. I think it's better to leave this one to the user to explicitly do their float->ratio conversion before attempting ops.

}
}
};
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about Rem?

fn description(&self) -> &'static str {
match *self {
fn description(self) -> &'static str {
match self {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you make this change? It's not really a problem either way, but it's totally unrelated to the rest of this PR.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe clippy caught this, and since it was minor I thought I'd go ahead and fix. I think there were a few more things that clippy suggested, but I held off on those since they were bigger changes. I'll keep these things out in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured... clippy means well, but this code is on the error path, unlikely to ever make any difference to performance. No worries though, the change is harmless.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, yeah good point.

use super::super::{Ratio, Rational};
#[cfg(feature = "bigint")]
use super::super::BigRational;
use super::super::{Ratio, Rational, Rational32, Rational64};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's great that you're adding so many tests -- actually impressive that this makes up the bulk of your PR! However, src/lib.rs is getting unwieldy. Can we move your additions to a new submodule in a separate file?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely. I wanted to do that, but didn't wanna rock the boat too much with my first PR here. :)

// }

test_isize(-2, _1, _NEG1);
test_i8(-2, _1, _NEG1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you've only tested with integers here and elsewhere, the math problems I mentioned with Add and Sub didn't come into question. (The denominators here are just 1.)

@turboladen
Copy link
Author

@cuviper thanks for the review! I hope to get to responding to all the things in the new few days. 🍻

@turboladen
Copy link
Author

I just remembered this PR and wanted to pop in and say that I had to backlog the work I was doing that depended on these changes. That work is still in my (day job) backlog but I’m not sure when I’ll get time to pick it back up.

@PTaylor-us
Copy link

Just wanted to give this a bump as I noticed the lack of primitive support as well. I have been creating Ratios from the integers to to the the math, but I don't think it's a great solution. If it would help, I might be able to take a look at the requested changes for this PR and see what I can do.

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

Successfully merging this pull request may close these issues.

None yet

3 participants