Open jakobkummerow opened 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.
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.
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:
- BigInts and Numbers are not semantically interchangeable. It is important for the developer to reason about them differently.
- 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.- These type-overloaded numeric operations cannot mix Numbers and BigInts, with the exception of comparison operations.
- Some numeric operations are not overloaded (such as unary
+
). The programmer has to remember which operations are overloaded and which ones are not.- 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 extendMath
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:
Math.log
accept Decimals? Does it accept BigInts?”.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.)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:
modInv
should be modExp
(or expMod
) to match the link target.pow
still mentions hypot
.sinh
twice.As for the "home for the functions" question, I do see advantages of BigInt.foo
over overloaded Math.foo
:
Math.
)+
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
, andMath.pow
are extended to also accept BigInts.The second option: create new
BigInt.sign
,BigInt.abs
, andBigInt.pow
methods. (Math.sign
,Math.abs
, andMath.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.
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 "truncatinglog2
")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 theBigInt
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 anexp
followed by a%
, ormodExp
because it's a "modularexp
"?modInv
because it's short, ormodularInverse
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.)