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

BigInt ceil, floor, round, and trunc?

js-choi opened this issue · comments

I originally did not include BigInt ceil, floor, round, and trunc. However, someone from TC39 (I think it was @ljharb) suggested that we add them as identity functions.

Yesterday, there was some pushback from other TC39 members about this. As far as I can tell, this is bikeshedding and both choices are harmless, but we’ll have to make a decision someday anyway.

CC: @ljharb, @michaelficarra, @waldemarhorwat

I'd rather not add them if they're not motivated by a use case, and I don't think we will find a use case.

there is a weird use case:
Imagine you are writing some code that works with both numbers ("safe integers") and bigints:
Math.trunc(a / b)
well.. it can be confusing to the reader...

@Yaffle This assumes we'll be extending the functions on Math to accept BigInts, which is not decided yet. It's possible that we'll be introducing new methods on BigInt, in which case it will always be clear what numeric type you are working with.

I don't think I was the one to suggest it.

I think it's fine to have them, and I think it's mildly bizarre that the current ones don't support BigInt.

The use case is "i shouldn't have to care if it's a number or a bigint when i'm doing an operation that makes sense for either".

I'll phrase it differently: I think BigInts and Numbers should always be interchangeable by default, unless that would cause silent precision loss - ie, unless there's a strong reason they should not be.

My preference is that the burden of proof should fall on the argument that both should not be supported, and/or not be mixed - and I agree "precision loss" when applicable is more than sufficient proof.

@Yaffle Trying to write code that works on both safe-integer Numbers and BigInts is fraught with pitfalls. Consider:
Math.ceil(3 / 2) === 2, but Math.ceil(3n / 2n) === 1n
Not encouraging such code would avoid such footguns.

@jakobkummerow , if you are not writing such code, so why do you need Math.ceil(bigint) which is doing nothing?

@Yaffle : that's my point, I don't see a reason for adding BigInt versions of these functions.

"Adding identity functions doesn't hurt" is not a compelling argument, because by that logic, why stop at four identity functions when we could add hundreds of them? Also, not adding identity functions doesn't hurt either.

"Numbers and BigInts should be interchangeable" is not a compelling argument, because there are many examples where they're clearly not interchangeable and never will be, due to their intentional differences.

Arrays and strings aren't interchangeable in many ways, and yet, they are interchangeable in many as well. The existence of incompatibilities in no way diminishes the usefulness of maximal compatibility when sensible.

Closing. ceil has now been completely removed due to potentially developer-surprising behavior: Math.ceil(3n / 2n) === 1n (#13 (comment): thanks @jakobkummerow and @syg). And if ceil is removed, then floor and friends are removed too.

See also #13 (comment) and #14 (comment).

commented

A use case is that one might want to generate a random 64bit number:

round( Math.random() * 0xffffffff_ffffffffn )

@streamich The problem with that snippet isn't that no suitable round exists.

The first problem is that Math.random() gives a Number, which you can't multiply with a BigInt like 0xf...n. And the reason the spec doesn't allow that is that it's entirely unclear what it should return: a Number or a BigInt?

The second problem is that Math.random() gives you 52 random bits. Multiplying that with a 64-bit value is about as useful as rolling a regular physical 6-sided die, multiplying the result with 100, and saying "now I have a 3-digit random number".

The third problem is that there are two kinds of off-by-one errors in this snippet: you don't want to round, and to get an N-bit random number, you don't want to multiply with 2**N - 1. Instead, you want to multiply with 2**N, and then truncate (aka "floor", for positive values). To illustrate, let's look at a smaller example: suppose you want to generate a random 4-bit number, and you define:

function BadRandom4Bit() { return Math.round(Math.random() * 0xF); }

You'd expect that to return one of 16 possible values, and if you run it often enough, you'd expect each of these values to be produced approximately the same number of times. For example, for 16 million invocations you'd expect each result to be returned approximately a million times:

let results = new Array(16).fill(0);
for (let i = 0; i < 16_000_000; i++) results[BadRandom4Bit()]++;
console.log(results);
// [535000, 1066301, 1066102, 1064489, 1066599, 1065193, 1067134, 1068806,
//  1067577, 1066025, 1066372, 1067442, 1067148, 1066188, 1066267, 533357]

Oops, rather than each value having a probability of 1/16, the first and last have a probability of 1/30, and the others 1/15. With a fixed version you can see what it's supposed to look like:

function GoodRandom4Bits() { return Math.floor(Math.random() * 0x10); }
let results_good = new Array(16).fill(0);
for (let i = 0; i < 16_000_000; i++) results_good[GoodRandom4Bits()]++;
console.log(results_good);
// [999847, 1000259, 999328, 999536, 999423, 999344, 1000096, 1000411,
//  1001997, 999682, 1000505, 999164, 999383, 999898, 1001393, 999734]

Summary: for generating random BigInts (with 64 bits or any other size), the most useful language primitive would be something like BigInt.random(), which could take a bit width parameter, or min/max parameters describing the desired range of results.

In the meantime, you can generate 64-bit random BigInts with:

function RandomBigInt64() {
  let low = Math.floor(Math.random() * 2**32);
  let high = Math.floor(Math.random() * 2**32);
  return (BigInt(high) << 32n) | BigInt(low);
}

In the meantime, you can generate 64-bit random BigInts with [...]

I’d add that in web platform and Node enviroments, there’s crypto.getRandomValues(new BigUint64Array(1))[0].

commented

@jakobkummerow yes, I mistyped it, it should not multiply number with bigint, I guess my snipped should be something like:

round( Math.random() * Number.MAX_SAFE_INTEGER )

I guess, the bigger problem is that bigint lacks a number of methods:

BigInt.random();
BigInt.mulWithNumber( Math.random(), 123n);

This is way too much ceremony for something this simple, and requires to call Math.random() twice (which already has 128bit entropy in Chrome I believe):

function RandomBigInt64() {
  let low = Math.floor(Math.random() * 2**32);
  let high = Math.floor(Math.random() * 2**32);
  return (BigInt(high) << 32n) | BigInt(low);
}

Another option would be to call Math.random() once and extract the high and low 32-bits, but that is even more code.


@bathos Your snippet will be exceptionally slow, it is about 100x slower than Math.random()! (Yes, I benchmarked it).

@streamich that’s interesting! but I’d note that if you need many random 64 bit integers and it’s in a hot path, you’d likely also want to change the size of the array accordingly (and possibly, reuse it) rather than call getRandomValues for each such number.

(Marking own-comment as off-topic though.)