tc39 / proposal-bigint-math

Draft specification for supporting BigInts in JavaScript’s Math methods.
https://tc39.es/proposal-bigint-math/
BSD 3-Clause "New" or "Revised" License
37 stars 2 forks source link

Possible different direction: `BigMathStrawman` #14

Open jakobkummerow opened 3 years ago

jakobkummerow commented 3 years ago

Discussions around this proposal have shown that extending Math functions with BigInt support is difficult (e.g.: transcendental functions are intractable; max/min run into surprisingly difficult "how to spec this?" questions for mixed inputs; etc), and while a few people have expressed support for getting a little closer to interchangeability of Numbers and BigInts, others have questioned the usefulness of such efforts.

At the same time, there is consistent demand for certain new, BigInt-only functions. Therefore, I'm inclined to think that bringing those into the language might be a "juicier" (more useful, more rewarding, more interesting) area for BigInt-related spec work.

In this very repository, issues #1, #2, and #12 are examples of such demand. The original BigInt proposal also collected four ideas for future proposals (with some overlap with the former set).

That leads to the following list of functions that could be spec'ed:

Of course, abs, sign, min, max as discussed here so far could also be included. (If they're even worth it; they're all one-liners.)

Where exactly to put these functions is an open question. I've used BigMathStrawman in the title in reference to an earlier discussion; probably the BigInt object is a good home for them, but there are alternatives.

The functions themselves also have various open questions. Some of them (in particular bitLength and *ByteArray, maybe the others too) need to decide how to handle negative BigInts. Some of them (expMod, gcd, modInv) can reasonably be implemented in userspace (which doesn't mean that there can be no value in adding them to the language), others (bitLength, fromString, to/fromByteArray) can get significant efficiency benefits from a native implementation (e.g. bitLength is at least O(log n) in userspace, but O(1) natively). Some of them could be renamed (expMod because it's conceptually an exp followed by a %, or modExp because it's a "modular exp"? modInv because it's short, or modularInverse because it's descriptive?) If this or another proposal decides to take on these functions, all those discussions can be had in detail.

(This is just a thought and an attempt to be constructive; feel free to close this issue if your mind is set that you don't want to go in this direction.)

js-choi commented 3 years ago

These are great ideas. However, the impression I got from the meeting (the notes should become public in a few days) was that the implementors would prefer if this proposal were contracted, rather than expanded. (@syg, @yulia, feel free to chime in.)

That is to say, I think I should focus this proposal on figuring out what operations that already exist are appropriate to extend, leaving new BigInt functionality to separate follow-on proposals. (How precisely to extend those already-existing operations is the fundamental question that I mean for this proposal to wrestle with, in #13, #10, and elsewhere.)

(I plan myself to propose popcount in a follow-on proposal, and if I have time I might do the same for modular exponentiation. Regarding fromString, there’s already @mathiasbynens’s https://github.com/tc39/proposal-number-fromstring, although it’s been in stasis for a couple of years now.)

Having said that, these ideas are much appreciated. I think it would be good to try to create a clear vision: When we do add more math functions like GCD, modular exponentiation, or popcount, where should we put them? Should they support both Number and BigInt? Should they be overloaded or separate methods? And so on.

mathiasbynens commented 3 years ago

FWIW, I’m not actively working on the fromString proposal right now, and I welcome other champions to step up and pursue it.

jakobkummerow commented 3 years ago

(I think when you say "yulia" you mean @codehag .)

js-choi commented 3 years ago

Yes, oops, I did mean @codehag.

Also, thanks, Mathias; good to know.

syg commented 3 years ago

The "contraction" is in the context of the original goal of supporting BigInt on existing Math operation. I take @jakobkummerow's point here to be that now you've reduced the scope, the remaining set of functions is both trivial but also met with design difficulties (like polymorphic min/max). At the same time, there is desire for numeric processing on BigInts. Given that, is the current scope worth our time over a different scope?

I'm biased here, but I also see this as another way to ask why is Number/BigInt interchangeability even in question here. As I said in plenary, I find "completeness" to be a very weak argument.

js-choi commented 3 years ago

Thanks for the comment, @syg. You’re right—as you said at plenary, the number of BigInt-overloadable Math methods is going to be small. So maybe it would be worth expanding the scope of this proposal to include new operations. I don’t know, and I’m willing to keep hearing opinions about it.

My vision has been to push through a very narrow set of Math overloads for BigInt and then add new operators piecemeal. (I think an overloaded Math.popcnt would make sense for both Numbers and BigInts, for example, and I was planning to make a proposal for it after this proposal advanced more.) But I’m open to changing my mind about that vision, with implementor feedback being my top consideration.

codehag commented 3 years ago

Yep, preference for reduction in scope of the proposal unless we have established usecases. +1 to @syg 's comment.

js-choi commented 3 years ago

I think I’ve settled pretty firmly on type-overloading a few select Math methods. I think this fulfills the goal of “maximal consistency with precedent”.

I’ve edited the explainer with the following:

Philosophy

The philosophy is to be consistent with the precedents already set by the language. These precedent include the following five rules:

  1. BigInts and Numbers are not semantically interchangeable. It is important for the developer to reason about them differently.
  2. But, for ease of use, many (but not all) numeric operations (such as division / and exponentiation **) are type overloaded to accept both Numbers and BigInts.
  3. These type-overloaded numeric operations cannot mix Numbers and BigInts, with the exception of comparison operations.
  4. Some numeric operations are not overloaded (such as unary +). The programmer has to remember which operations are overloaded and which ones are not.
  5. asm.js is still important, and operations on which it depends are not type overloaded.

In this precedent, only syntactic operators are currently considered as math operations. We extend this precedent such that Math methods are also considered math operations.

Vision

This initial proposal overloads only a few first Math methods. The vision is that this proposal would open up the way to new proposals that would further extend Math with type-overloaded methods. These may include:

I think this approach is more consistent than adding a few select methods to BigInt. We’re either going to require developers to:

I think these two approaches are basically equivalent in memorization burden. And both approaches throw TypeErrors when invalid operations are attempted on invalid types. But I think the former approach (type-overloaded math methods with some exceptions) is more consistent with precedence (type-overloaded operations with some exceptions like unary +).

jakobkummerow commented 3 years ago

I approve of dropping all the functions that don't make sense for BigInts or have no known use cases.

One could take this a bit further still:


Comments on the current version of the explainer:


As for the "home for the functions" question, I do see advantages of BigInt.foo over overloaded Math.foo:


I'd also like to point out that "maximal consistency with precedent" can't be defined as an absolute truth, there's always some amount of personal perspective involved. For example, my own perspective is that I don't see a precedent of "overloading some but not all operations", I see a precedent of "overloading operators (for lack of viable alternative), and not overloading any functions". (To emphasize, I'm not arguing that my perspective is more "correct" than yours, just pointing out that personal mental models play a role for the question of what's consistent.)

js-choi commented 3 years ago

@jakobkummerow: I think these are all reasonable concerns. As you suggest, this issue is partially a question of trade-offs and partially a question of personal perspective.

It’s true that I have my own opinions (being inclined towards overloading Math methods, because that is what I would expect as a developer). But I’m not the most important person: consensus among the Committee (including from the engine implementers) is most important. I will try to ask other TC39 members about what to do about this issue in the next plenary meeting, and see what they all have to say there.

I will also try to work with the TC39 Research Incubator team (co-led by @codehag) to assess a couple of research questions over hopefully the next few months. These would include, among other questions, “Should BigInt sign/abs/pow live in the Math object or in the BigInt object?”

The first option: Math.sign, Math.abs, and Math.pow are extended to also accept BigInts.

The second option: create new BigInt.sign, BigInt.abs, and BigInt.pow methods. (Math.sign, Math.abs, and Math.pow would continue to accept only Numbers).

Option Memorization
Extend Math.sign etc. The developer has to memorize which Math methods also accept BigInts.
Create BigInt.sign etc. The developer has to memorize what methods does the BigInt object have.

Hopefully, discussion at plenary and also the research team would be able to clarify this dilemma over the next several months.

jakobkummerow commented 3 years ago

Just keep in mind that lack of engagement might also mean that people (who all have limited time!) just don't care all that much about a handful of one-liners:

BigInt.sign = (x) => x > 0 ? 1n : x < 0 ? -1n : 0n;
BigInt.abs = (x) => x < 0 ? -x : x;
BigInt.pow = (x, y) => x ** y;
BigInt.min = function() { return [].reduce.call(arguments, (a, b) => a < b ? a : b); }
BigInt.max = function() { return [].reduce.call(arguments, (a, b) => a > b ? a : b); }

// Or the two-argument version, which is probably the majority use case:
BigInt.min2 = (x, y) => x < y ? x : y;
BigInt.max2 = (x, y) => x > y ? x : y;

Anyone who needs these can just copy-paste that snippet (or spend about two minutes to independently re-invent it), without several months of research effort :-)

js-choi commented 3 years ago

I presented a brief update presentation about this issue to the Committee at the October plenary today. I tried to emphasize the symmetric tradeoff between “developers memorizing a table of which Math functions are polymorphic” versus “developers memorizing a table of which Math functions are also in BigMath”.

@sarahghp of Igalia, approaching this from championing Decimal, expressed strong support for Math polymorphism rather than separate globals. @syg asked @sarahghp for clarification about her position toward having a DecMath in addition to Math and BigMath, and @sarahghp affirmed that they would be opposed to a Math/BigMath/DecMath system rather than polymorphic Math.

I plan to continue the selectively polymorphic Math approach before presenting for Stage 2 in a few months, barring signals from other representatives that they would hard block Stage 2 over this issue.