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

+/- infinity? #53

Closed
ckaran opened this issue Jul 12, 2019 · 20 comments
Closed

+/- infinity? #53

ckaran opened this issue Jul 12, 2019 · 20 comments

Comments

@ckaran
Copy link

ckaran commented Jul 12, 2019

Is it possible to add support for +/- infinity? The rational equivalents would be logically be 1/0 and -1/0. This would allow conversion of float('inf') and -float('inf').

@cuviper
Copy link
Member

cuviper commented Jul 12, 2019

That representation is straightforward. The harder part is defining the semantics of all the operations.

For example, in general arithmetic, should we try to map numerator overflows to infinity? That may be fine for integers (denominator=1), but it's harder for other denominators. If we do overflow to infinity, should we also underflow (on denominator overflow) to zero?

What about operations with infinity where floating point would give NaN?

@cuviper
Copy link
Member

cuviper commented Jul 12, 2019

As a prior-art counterpoint, Python's Fraction does not support this.

@ckaran
Copy link
Author

ckaran commented Jul 12, 2019

How do IEEE 754 numbers handle overflow/underflow? That is probably the way to go.

As for operations that give NaN, I'm not sure how to handle it. That said, the only value that I can think of that would result in a NaN would be 0/0; all others would either be a finite value, or would round up or down towards infinity or zero. Can you think of any operations that would yield NaN?

@cuviper
Copy link
Member

cuviper commented Jul 12, 2019

How do IEEE 754 numbers handle overflow/underflow? That is probably the way to go.

That's fine as a guiding principle, but I think that will be challenging to implement, especially for generic integer types.

And consider cases like (100i8 / 3) * (2 / 1) -- ideally this is 200 / 3, but that can't fit in i8. Infinity is not the right answer here, because the ratio is still less than our max finite value 127i8 / 1. In floating point, inexact answers would just be rounded, but that's a huge complication to consider adding here.

The main point of Ratio is to keep exact rational numbers. If we start introducing float-like imprecise behavior, I wonder why wouldn't you just use a real float in the first place?

That said, the only value that I can think of that would result in a NaN would be 0/0; all others would either be a finite value, or would round up or down towards infinity or zero. Can you think of any operations that would yield NaN?

0 / 0 is a decent integral representation of NaN, at least.

Other NaN operations off the top of my head: inf / inf, 0 * inf, inf - inf, -inf + inf, inf % x

@ckaran
Copy link
Author

ckaran commented Jul 12, 2019

And consider cases like (100i8 / 3) * (2 / 1) -- ideally this is 200 / 3, but that can't fit in i8. Infinity is not the right answer here, because the ratio is still less than our max finite value 127i8 / 1. In floating point, inexact answers would just be rounded, but that's a huge complication to consider adding here.

Yeah, I was really only thinking about BigRational here... I see your point about it not working in general though, and I'm not sure how you'd make it work.

The main point of Ratio is to keep exact rational numbers. If we start introducing float-like imprecise behavior, I wonder why wouldn't you just use a real float in the first place?

I agree with you, the imprecision of floating point numbers is the reason why I'm using this crate in the first place (I'm dealing with the fallout of using a library that uses floating point numbers to represent time. I'm rewriting the library, but infinity is one of my many headaches with the library).

0 / 0 is a decent integral representation of NaN, at least.

OK, then maybe that will be a good representation? I think that as soon as you try to reduce any rational after you've done an operation, you'll end up with 0/0 again, correct?

@cuviper
Copy link
Member

cuviper commented Jul 12, 2019

(I'm dealing with the fallout of using a library that uses floating point numbers to represent time. I'm rewriting the library, but infinity is one of my many headaches with the library)

How does infinity come up in representation of time?!?

I wonder if it might be better for you to wrap such values at a higher layer, like:

enum Value {
    Rational(BigRational),
    Infinity,
}

... or whatever variants make sense in your domain.

@ckaran
Copy link
Author

ckaran commented Jul 13, 2019

How does infinity come up in representation of time?!?

It's actually a really clever trick. The library is maintaining a minimum priority queue of future events sorted by date. The events themselves contain closures that are executed as they are popped off the queue. The last event is always a sentinel whose job is to go do a bunch of work before scheduling a new set of events. The issue is that events can generate more events, which all occur at a finite moment in time. So as long as events are being generated, they'll be executed before the sentinel is. The event execution system remains completely oblivious to the fact that work needs to be done by the sentinel; it just keeps executing events until there aren't any more.

@ckaran
Copy link
Author

ckaran commented Jul 13, 2019

As for wrapping a BigRational, I'd rather avoid doing that as it would lead a LOT of code rewrite; either I'd have to implement the all of the standard operations over again on the wrapped value, or I'd have boatloads of match statements in the library. Convincing you about adding in infinity is easier! 😜

@cuviper
Copy link
Member

cuviper commented Jul 13, 2019

Rust is pretty good for representing such special values at the type level, rather than having to treat it as a sort-of-real numeric value. My first thought is to translate that to Option<Ratio>, where None is your sentinel.

If you really want, you can already use Ratio::new_raw with your special value, like any x/0 that's not supposed to happen normally. It doesn't have to act like infinity if you never actually do operations on the sentinel value, right?

@ckaran
Copy link
Author

ckaran commented Jul 13, 2019

Do comparisons count as operations? The event queue is comparing by time...

@cuviper
Copy link
Member

cuviper commented Jul 13, 2019

Do comparisons count as operations?

Yes. For this, you could use a singleton wrapper struct Foo(Ratio) with your own comparisons for sorting the queue, and whatever method pops from the queue can just unwrap it directly foo.0. But this is also true if you used an Option<Ratio> or any bespoke enum too -- if the alternate value just goes to generate more events internally, the rest of the code never has to see this detail.

Convincing you about adding in infinity is easier! 😜

I get the humorous tone, but... it really seems like you can solve this better at a higher level.

@ckaran
Copy link
Author

ckaran commented Jul 13, 2019

I suspect that you're right for this particular library. However, if we review the operations you mentioned earlier, I think that using 1/0 for infinity and 0/0 will get us a long ways along:

  • inf / inf can be reduced as follows: (1/0) / (1/0) -> (1/0) * (0 / 1) -> 0/0
  • 0 * inf can be reduced as follows: (0/1) * (1 / 0) -> 0/0
  • inf - inf can be reduced as follows: (1 / 0) - (1 / 0) -> (1 - 1) / (0 - 0) -> 0/0
  • -inf + inf is the same as above

Other operations all require you to have a common denominator, which means that a/b and c/d will both become (a*d) / (b*d) and (c * b) / (b*d). So if c/d is 1/0, the first will 0/0 and the second will be b/0, which we can reduce to 1/0 by convention. Likewise, if c/d = 0/0, then both the left and right reductions would become 0/0. That will ensure that NaN propagates through any operation.

@cuviper
Copy link
Member

cuviper commented Jul 13, 2019

I believe it's possible to define semantics for everything, but I doubt that we should.

@ckaran
Copy link
Author

ckaran commented Jul 13, 2019

OK, so you're against including infinity in the library?

@cuviper
Copy link
Member

cuviper commented Jul 13, 2019

I'm against -- not so hard as to close this outright, but I don't think there's sufficient motivation yet. I feel if we were to add such things as infinity and NaN, we would need to be really thorough about all of the semantics and corner cases.

@ckaran
Copy link
Author

ckaran commented Jul 15, 2019

I can see why you'd be against; I spent the weekend really thinking this through, especially what you said earlier:

The main point of Ratio is to keep exact rational numbers.

and

And consider cases like (100i8 / 3) * (2 / 1) -- ideally this is 200 / 3, but that can't fit in i8. Infinity is not the right answer here, because the ratio is still less than our max finite value 127i8 / 1. In floating point, inexact answers would just be rounded, but that's a huge complication to consider adding here.

The problem with what I'm proposing here is that it changes the underlying semantics of Ratio so that it is no longer exact. What's more, what I'm really after is a BigFloat type that implements all of Float. Ratio types can't implement Float because of the transcendental functions. What's more, some methods/functions simply don't make sense in a Ratio type of context (e.g. max_value(), min_value(), min_positive_value(), epsilon()).

Given all this, would you be interested in a new crate that is built on BigRational that has a built-in concept of precision that would be a part of the num family? If so, I'll close the two issues I've got open (this and #54), and open a new issue dedicated to brainstorming the semantics of the BigFloat crate.

@cuviper
Copy link
Member

cuviper commented Jul 15, 2019

What's more, what I'm really after is a BigFloat type that implements all of Float.

Unfortunately, Float requires Copy, which will never work for a type containing BigInt or the like. You could still mimic most of those methods, but there's no way to actually implement Float.

Depending on your needs and rounding semantics, Ratio<i64> or Ratio<i128> might be big enough as a Copy-able type, but not if you want arbitrary sizes.

Given all this, would you be interested in a new crate that is built on BigRational that has a built-in concept of precision that would be a part of the num family?

I think that sounds like a nice crate idea, but I'd rather see it developed independently. Frankly, I don't give enough time to the num family of crates as it is...

brainstorming the semantics of the BigFloat crate.

I'd suggest thinking about how this will be different than a BigDecimal sort of crate, of which there are several already. One thing is that truly rational numbers like 1/3 can be represented exactly, but then your rounding semantics become very important -- defining when values are not exact.

@ckaran
Copy link
Author

ckaran commented Jul 15, 2019

Unfortunately, Float requires Copy, which will never work for a type containing BigInt or the like. You could still mimic most of those methods, but there's no way to actually implement Float.

Yup, you're right, I missed that bound.

Depending on your needs and rounding semantics, Ratio<i64> or Ratio<i128> might be big enough as a Copy-able type, but not if you want arbitrary sizes.

Did I mention that I need both sin() and cos()? Basically, I think that you're right about finding a BigDecimal crate, and hoping it provides everything I need. Do you have any that you recommend?

but then your rounding semantics become very important -- defining when values are not exact.

That was part of what I was wrestling with earlier today. If the BigFloat type has a built-in precision, then each instance could have a different precision. What is the best way to handle that???

At this point, I think I'm going to drop the idea of adding INFINITY or NaN to Ratio; it complicates things too much.

@cuviper
Copy link
Member

cuviper commented Jul 15, 2019

Basically, I think that you're right about finding a BigDecimal crate, and hoping it provides everything I need. Do you have any that you recommend?

I don't have direct experience using any, but there are a few mentioned in rust-num/num#377

That was part of what I was wrestling with earlier today. If the BigFloat type has a built-in precision, then each instance could have a different precision. What is the best way to handle that???

If "built-in precision" means they have some field specifying precision, then you could go by the rules for significant figures, which generally output the least significance among all operands. I would also treat exact rational numbers as having maximum/infinite precision.

"Built-in precision" might also mean as a const generic parameter, in which case you can choose not to mix precisions at all at the type level, or else make such mixing becomes more explicit in implementations.

@ckaran
Copy link
Author

ckaran commented Jul 15, 2019

If "built-in precision" means they have some field specifying precision, then you could go by the rules for significant figures, which generally output the least significance among all operands. I would also treat exact rational numbers as having maximum/infinite precision.

That was pretty much what I was thinking, although I need to spend some more time really thinking about what this might mean in the context of non-linear functions, such as tan(). If I have a BigFloat a, with a precision of +/- 0.0001, then near pi/2 this can yield wildly varying results. I'm not sure what the best way to handle this might be as the value crosses from -infinity to +infinity...

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

2 participants