tc39 / proposal-math-sum

TC39 proposal to add a summation method to JavaScript
https://tc39.github.io/proposal-math-sum
76 stars 2 forks source link

sumPrecise name and "floating point" language may be misleading #14

Open karlhorky opened 5 months ago

karlhorky commented 5 months ago

Hi @bakkot, thanks for this proposal!

When I was reading the Math.sumPrecise proposal tweet from @robpalme, I somehow read into it that it would perform precise floating point math, to avoid the classic 0.1 + 0.2 = 0.30000000000000004 unusual behavior from JavaScript.

But running the polyfill in Node.js does not lead to other results:

const Math2 = await import('./sum.mjs')
Math2.sum([0.1, 0.2])
> 0.30000000000000004

But this appears to be a separate issue, which @jessealama @apaprocki @jirkamarsik and previously @littledan have worked on in the Decimal proposal:

0.1m + 0.2m = 0.3m

I wonder if the name sumPrecise would confuse other users (as it did me) enough to consider it when looking for solutions for floating point arithmetic problems in JS.

bakkot commented 5 months ago

Yeah, we were aware that there would be this confusion. But the name does need to somehow indicate that this is more precise than normal floating point summation, and I think any name with that property would confuse people as it did you. So I don't see a way to avoid this problem. At some point people will have to learn how floating point numbers actually work.

For what it's worth, this does perform precise floating point math. The problem here is that writing 0.1 does not represent the real number 0.1 but rather the real number 0.1000000000000000055511151231257827021181583404541015625. Similarly for 0.2 and 0.3. If you add the real number actually represented by 0.1 to the real number actually represented by 0.2, and then take the closest JavaScript number value to the result, that is gives you 0.30000000000000004.

That doesn't generalize to adding more than 2 numbers normally - x + y + z is not necessarily the same as adding the real number represented by x to the real number represented by y and the real number represented by z - but does when using sumPrecise.

karlhorky commented 5 months ago

Understood, the naming does pose a dilemma. I wonder if there would be any other naming options which could describe properties of this solution in an alternative way 🤔

Anyway, short of finding another suitable name, happy to either leave this open for visibility to others or close it - whatever fits wishes of the project.

ptomato commented 5 months ago

It was originally called Math.sumExact. I thought Math.sumPrecise was less likely than sumExact to cause that confusion, though still fairly likely (and I couldn't really articulate why I thought sumPrecise was better, either.)

No one made any other suggestions. I half-suggested Math.fsum since that's what the Python method is named, but I don't really like that either because it's just not informative at all.

karlhorky commented 5 months ago

Reading through a bit of Shewchuk's paper and researching about related material, 2 additional suggestions came to mind:

bakkot commented 5 months ago

Neither of those really suggest "this is more precise than naive summation".

Event-Horizon commented 5 months ago

May I suggest:

Something with -er postfix suggests its better precision but not exact precision.

Protonk commented 4 months ago

Rather than treating the problem of user confusion that Math.sumPrecise([0.1, 0.1, 0.1]) doesn't equal 0.3 exactly as something to be solved by renaming the method, I think it would make more sense to name it in a manner that would not need to be changed after proposal-decimal is added into TC39. Once that proposal lands, a method like this will return an exact sum and it won't be reasonable to then change the name.

I like Math.sumExact because AFAICT the proposal here doesn't implement the full adaptive range in Shewchuk's paper and instead just goes to arbitrary precision to return an exact result, however there are some other options:

@Event-Horizon's suggestion of an -er postfix could be good and would still be a valid name after proposal-decimal lands.

I also support keeping the name Math.sumPrecise because 1.) it is a heavy load to put on a proposal like this to educate users and programmers about binary and decimal representations and 2.) a name with that aim in mind may not do much to accomplish it.

bakkot commented 4 months ago

Once that proposal lands, a method like this will return an exact sum and it won't be reasonable to then change the name.

This method will never accept Decimals (for the same reason it doesn't accept BigInts), so I don't think the possible addition of a Decimal type to the language has much bearing here.

Protonk commented 4 months ago

Ok. Sorry for my confusion. Nevertheless I think we should not worry too much about picking a method name in order to educate users on floating point in JS generally. Rather we should pick one that describes the method well, which Math.sumPrecise does just fine.

RyanKadri commented 3 months ago

Sorry if this is an ignorant question - Does this method's name need to express the fact that it's doing more precise summation? The current README says:

Summing a list is a very common operation and is one of the few remaining use cases for Array.prototype.reduce. Better to let users express that operation directly. and also Also, summing a list of floating point numbers can be done more precisely ... a fact which few JavaScript programmers are aware of

How much slower is this precise approach? At what point would I not want to reach for it? Unless this method is much slower and likely to be a footgun, would it be better to just call it Math.sum and consider a Math.looseSum/Math.impreciseSum or something for the naive implementation? It feels like the second would be less likely to lead to confusion if the 99% use-case is compatible with the "more correct" implementation.

(Asking because I worry about the same confusion with the "precise" wording)

If the risk of performance footgun is high, I'd vote for Math.strictSum. I actually think the ambiguity around what strict means is more likely to get users to look up the method and understand the use-case and tradeoffs

bakkot commented 3 months ago

Optimistically for lists of > 100 elements it will be perhaps 4x slower than naive summation implemented natively would be, but that's just from eyeballing graphs from this post. As a user it will still probably be faster than your own code, at least until your code gets into a higher optimization tier in the engine. Though to be clear, these are at this point just my personal guesses; it's hard to know this sort of thing prior to actual implementations.

Whether that slowdown matters depends on your perspective, I suppose. Engines expressed reservations about using the name "sum" for anything more complicated than naive summation.

strictSum isn't a terrible name, though I'd probably spell it sumStrict for discoverability reasons. I don't know that we'd really be benefitting users by hiding fact that this algorithm is in fact as precise as you can get, though - if they're asking the computer to do math, they're going to have to learn about its limitations at some point.

HansBrende commented 2 months ago

Just want to echo the thought that Math.sum is the obvious choice here, since correctness should be the default; therefore, any summation algorithm that gave incorrect results should be the one called Math.sumInexact rather than the other way around.

IEEE standard requires that all x + y is correctly rounded to 53 bits of precision, however notice they did not introduce a new operator "+precise" that did this. To my mind, the semantics of Math.sum would naturally follow then from the precision requirements of +. (What other semantics could it have that wouldn't be horrible?)

However, if that's not on the table, Math.sumExact or Math.sumPrecise are both better in my mind than Math.sumStrict, as the vagueness of Math.sumStrict doesn't really convey anything at all to me.

Of the former two, Math.sumPrecise seems more accurate than Math.sumExact since technically the result will be rounded towards the nearest floating point number--therefore not necessarily "exact" per se. An exact summation would require more than 53 bits of precision in some cases.

RyanKadri commented 2 months ago

Generally agree on the "correct" option just being Math.sum() provided there's not a huge performance footgun.

If not that naming though, I think the ambiguity in the sumStrict is actually a feature rather than a "bug". I think users are more likely to make assumptions about sumExact or sumPrecise whereas sumStrict is ambiguous enough that it kind of requires a docs search to figure out what it means.

I think that's actually desirable since the "strictness" has some performance tradeoffs and a fairly nuanced meaning beyond what you'd assume from "precise". I don't think a short name can clearly convey the nuance of what this proposal aims to do, so a less clear short name is actually a bit better (again if the plain Math.sum is not appropriate)

HansBrende commented 2 months ago

@RyanKadri the semantics are simply: "the result is completely correct to the available precision".

Which I'd argue is exactly what you would expect from sumPrecise() (or sum(), for that matter!) The only question a developer might have would be: "Precise? How precise?" Which would be answered by the documentation.

Far more nuanced would be the semantics of a naive summation's output: "the result may be correct, or completely wrong, depending on a myriad of edge-cases".

(sumStrict() on the other hand could mean anything under the sun; for example, it could mean that the summation throws an error if the result overflows. It could also mean the opposite of the intended semantics, as in the sense of Java's strictfp keyword which indicates that higher precision than 53 is not used during calculations.)

codehag commented 1 week ago

This might be a dumb question, I'm not terribly familiar with this space:

We already have Math.fround and Math.f16round, why we are not using Math.fsum here? It may opaque to new users who have never used floating point precise methods, but it feels like choosing another name breaks with consistency.

I might be seeing similarity where there is none though: My understanding of the spec, and also reading the paper is that the implementation can rely on the same mechanism (IEEE's round-ties-to-even) as we currently use for fround and f16round. I would expect someone familiar with these methods would similarly expect this to be named fsum. But maybe I am mistaken and sumPrecise has no relationship to these methods?

If it does have a relationship to those methods, I think we might want to consider some other questions. what happens if we need f16 sums? What happens if other methods are added later? Should we have aliasing now?

bakkot commented 1 week ago

"Math.fround" means "round to the nearest JS number which is precisely representable as a 32-bit float". Here we're doing maximally precise 64-bit summation. So I think suggesting a similarity between the two would be misleading; I've always assumed that "f" in "fround" stands for the C type float (i.e., 32-bit float), by contrast to double (i.e., 64-bit float) which is what JS numbers are. But this operation works only in double space, not float.

So, no, I don't think there's much relationship to fround or f16round.

codehag commented 1 week ago

thanks for the clarification!