tc39 / proposal-bigint-math

Draft specification for supporting BigInts in JavaScript’s Math methods.

Home Page:https://tc39.es/proposal-bigint-math/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Possible different direction: `BigMathStrawman`

jakobkummerow opened this issue · comments

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:

  • bitLength (subsumes "truncating log2")
  • expMod (combined exponentiation and modulus)
  • fromString(..., radix)
  • gcd (greatest common divisor)
  • modInv (modular multiplicative inverse)
  • toByteArray, fromByteArray

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.)

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.

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

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

Yes, oops, I did mean @codehag.

Also, thanks, Mathias; good to know.

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.

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.

commented

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

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:

  • Memorize which operations accept BigInts/Decimals: “Does Math.log accept Decimals? Does it accept BigInts?”.
  • Memorize which methods are present on the Decimal/BigInt objects: “Does Decimal.log exist? Does BigInt.log exist?”

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 +).

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:

  • Math.pow has no use case insofar as it's just a more verbose version of **. (The reason it exists for Numbers is that it predates ** by many years, as it's existed since ES1. The introduction of the ** operator in ES2016 has made Math.pow useless. There's little reason to extend the capabilities of a now-useless legacy function.)
  • When working with 32-bit numeric values, for which clz32 would return useful results, it's officially recommended to stick with Numbers. As the name "BigInt" implies, they're meant for bigger-than-Number use cases. (And those cases are better served by bitLength than clz64, but that's a separate topic.)

Comments on the current version of the explainer:

  • There's a typo in the "Vision" section: the first occurrence of modInv should be modExp (or expMod) to match the link target.
  • The "Description" section still contains "Its philosophy is that BigInts and Numbers should always be interchangeable by default, unless", which is now outdated.
  • The footnote for pow still mentions hypot.
  • The "Excluded Math" list contains sinh twice.

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

  • slightly faster in implementations, as no value's type has to be compared repeatedly
  • easier to support with tooling (enumerating properties of an object is easier than enumerating acceptable types of the arguments of a built-in function; see e.g. Chrome/Firefox developer consoles providing completions when you type Math.)
  • it is arguably a cleaner design to try to avoid exceptions whenever possible; in that sense "all Math.* work only on Numbers, all BigInt.* work only on BigInts [and in a possible future with Decimal, all Decimal.* work only on Decimals]" is cleaner than "most Math.* functions work only for Numbers, but there are some exceptions that also work for BigInts, very few even work for mixed inputs [and we may add one or two that are only spec'ed for BigInts, and once Decimal comes along, it'll get even more confusing]". (I don't think the fact that + and >>> unfortunately couldn't/can't be overloaded for BigInts is a convincing counter-argument to a general desire to avoid special cases.)

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.)

@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.

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 :-)

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.