-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Use unlimited MathContext for BigDecimal arithmetic #3303
Use unlimited MathContext for BigDecimal arithmetic #3303
Conversation
Codecov Report
@@ Coverage Diff @@
## master #3303 +/- ##
==========================================
+ Coverage 93.15% 93.15% +<.01%
==========================================
Files 378 378
Lines 7579 7585 +6
Branches 203 210 +7
==========================================
+ Hits 7060 7066 +6
Misses 519 519
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gosh. Yeah this makes sense to me, but in the event someone is surprised by x + y
not equaling x |+| y
and .sum
not equalling .combineAll
it might be good to add a comment.
If the MathContext was part of the type signature of BigDecimal - in pseudocode maybe something like At any rate, part of the problem is certainly that BigDecimals with differing contexts definitely are not the same type. |
Just to make sure I understand, the old behaviour would add the numbers using an unlimited context, then truncate them using the lhs' MathContext? The new behaviour adds them using the lhs' MathContext? |
Another slant on this is that it reminds me of some monoidal data sketches (e.g. t-digests) where in general s1 |+| s2 is not exactly equal to |s2| + |s1| however it is the case that they are always very similar, for some appropriate definition of "similar" (and likewise for associativity). These kind of cases have had me wondering in the past if there is some useful epsilon/delta formalism for objects that are not formally associative or commutative, but are "within epsilon of exact associativity, with probability 1-delta", etc |
@tixxit There's no truncation on 2.13 with this change, or on 2.12 in either case, since Scala's Without this change on 2.13 the context of the lhs is used to add (subtract, etc.), but there's no explicit rounding. After this change on 2.13 (and in either case on 2.12) unlimited precision is used to add, and the lhs's context is propagated, but there's still no rounding. I guess it's worth noting that the propagation means that scala> (x |+| y).mc == (y |+| x).mc
res13: Boolean = false …but I can't make myself care much about this, and throwing away the contexts instead of propagating the lhs's seems likely to be surprising to users. |
I'm fine with this change, especially if we document the difference between |
It's pretty easy to break associativity with non- |
Agreed, but I think that would fit better somewhere like Algebra or Spire. In cats-kernel I think it's reasonable just to say "we'll perform operations in a way that doesn't throw out precision, if you want to do that you can do it yourself". |
@tpolecat Added a comment, thanks for the suggestion! |
What about defining an order in MathContext and then taking the max or min rather than taking the left? I can see an argument for taking both the max and the min. I’d probably lean towards the max. |
I like this in theory but I think it's going to break expectations. Take the following: myUnlimitedPrecisionValue |+| BigDecimal(1) In my view this pretty much absolutely must not return a limited-precision result, so we have to take the max, but then the following returns a myDecimal32Value |+| BigDecimal(1) …which also seems kind of weird? I wish this was properly commutative with respect to context, but I also think that only matters to people who care about the context, who are also likely to expect the standard "use the lhs's context" behavior (which has been around forever, not just in 2.13). |
@johnynek There are other issues with ordering The bigger issue (apart from the user expectation problem) is that it seems really strange to have an ordering on contexts that we use for these operations but ignore in our ordering on My feeling is that Cats should follow the standard library's approach to propagating contexts but otherwise pretend they don't exist. This gets us uniform behavior across Scala versions, reliable associativity everywhere, and reliable commutativity as long as you don't look at |
@johnynek Are you okay with the solution in this PR? It doesn't make the commutativity situation with respect to We just hit another spurious test failure CI (#3316) because of this issue. |
I’m not really happy about it, but I am not trying to block it. If it isn’t commutative (I’m on my phone now) we should make sure it doesn’t extend commutative group (or monoid etc...) I’d rather define an Ordering on MathContext and use that as I said. Or remove the implicit instance of the typeclasses and require a MathContext to construct the instance. |
@johnynek To summarize: Right now we have an implicit
This PR fixes the latter three problems, but doesn't change any behavior related to the first, and doesn't really have anything to do with the first. I've opened #3317 to track the first separately. |
Scala 2.13 changes the behavior of arithmetic on
BigDecimal
so that theMathContext
of the first operand is used (instead of always usingUNLIMITED
, which is what was done in previous Scala versions; see this issue for some discussion of the change).While this change is arguably the right thing to do in the standard library (assuming we're stuck with the mistake of carrying around a context in the first place), I think I've convinced myself that it's the wrong thing to do in Cats's instances. For one thing the 2.13 change means that operations from these instances are no longer commutative or even associative, even for fairly simple cases:
And then:
…while on 2.12 both of these results are
true
.These issues don't turn up when we're checking the laws right now, because we have this instance in
cats.kernel.laws.Tests
:This workaround seems to me like a bad idea to start with, and for more complex test cases it's not enough, anyway (see for example the issue @johnynek ran into in #3279).
This PR changes the implementations of all of the operations that require contexts back to the 2.12 behavior (it has no effect on 2.12). I've also removed the custom
Arbitrary[BigDecimal]
in the cats-kernel-laws tests. It precedes 2.13 (it was introduced in #1319 in 2016), but removing it doesn't seem to cause problems in a few test runs I've just done on both 2.12 and 2.13. If it turns out that we needed it to avoid flakiness I'll revert that part.