A more complete random API?

Right now, in Javascript, our main source of randomness comes from a single function: Math.random(). I'm wondering if there would be interest in adding a more full-blown random library to Javascript, and if so, what kinds of random methods should exist in this library?

Some requirements:

  • It should provide easy-to-use methods for common random needs (i.e. provide a Random.randInt(start, end) instead of having people use common and incorrect patterns we see everywhere today, such as Math.floor(Math.random() * (end - start)) + start. It's also important to provide proper implementation of random logic that's difficult to implement correctly by hand, like Random.shuffle(array).
  • It should be seed-able. i.e. something like const myRandGen = new Random(<seed>); myRandGen.randInt(start, end)

Just to get things started, here's a handful of methods that a new Random API could include:

Random.randInt(2, 5) // Either returns 2, 3, or 4
Random.randBigInt(2n, 5n)
Random.randBool() // Either true or false
Random.randFloat(start, end) // A float where start <= x < end

Random.choice(['a', 'b', 'c']) // Reteurns a random entry from the array
Random.choice('abc') // Returns a random character
const newArray = Random.shuffle(['a', 'b', 'c'])
Random.shuffleInPlace(myArray)

const gen = new Random(<optional seed>)
gen.randInt(...) // The instance constaints all of the static methods.
const newGen = gen.clone() // Clones the pseudo-random-number generator at its current state
console.assert(gen.randBool() === newGen.randBool())

We could also provide methods to provide random values using other distributions besides a linear one. It's probably wise not to stick every possible random-related thing into this, anyone who's wanting to do deep statistical logic, or to build some game that deeply depends on random generation should probably install a more full-featured random library. The build-in API should just try and cover the most common use-cases, so people don't have to keep twiddling with floats from Math.random() to do everyday tasks.


A note on Random.shuffle()

Random.shuffle() (and Random.choice()) has been discussed previously on this form here. Over there, @claudiameadows brought up an interesting point that I was unaware of.

Shuffling's way harder than you'd think to do right - the gold standard for it requires enough bits of RNG state to hold all possible mutations in order to hit all permutations and for all but small arrays (of <= 34 entries), they can't even use what they use for Math.random().
...
And yes, I'm saying that Python's default is actually very bad.

I think this is an argument for why the Javascript language needs a random shuffle method. Any home-grown one is likely to be implemented worse than what we can do natively in Javascript. And yes, maybe Javascript can't provide a truly random shuffle method, but we can reflect this shortcoming in the method name, so it's clear to users that the shuffle method is not perfect. For example, Random.semiRandomShuffle(array). How to actually name it can be debated, but providing a proper shuffle method with a name that indicates its shortcomings is better than not having one at all.


prior art:

5 Likes

Exactly!

Existing proposal for seeded random

1 Like

That looks very related. It unfortunately only provides seeded random number generation, not helper functions such as randInt(), which was my bigger interest here. Maybe it'll be better to move this discussion onto that proposal and see if they're willing to expand the proposal.

Actually, as I've looked closer at that proposal, I don't really see a feasible way of trying to move this conversation there. The idea of a fuller random library seems to be completely out of the scope of that proposal and would need to be a proposal of its own, that happens to have some overlapping feature.

But, they do have some good thoughts going on there. Like, the idea of having all implementations use the same pseudo-random-number-generator, so that you can use the same seed on any JavaScript platform and receive the same results.

1 Like

Note: the Mersenne Twister fails numerous statistical tests for randomness. It's only used because of the massive state size

I would find a randomRange implementation really useful as it would eliminate having to calculate rounding vs truncating values all the time and would also make it specific as to what was being requested.

Something like Math.randomRange(low=0, high=100, step=1)

1 Like

Related proposal by @tabatkins GitHub - tc39/proposal-random-functions: Proposal to add a Random namespace and several additional convenience functions for using randomness.

2 Likes

There is a question with this one that concerns me. JS, at least conventionally, uses 16bit characters. Consequently, Unicode codepoints outside of plane 0 are typically rendered using pairs of characters. These are usually referred to as surrogates.

So if I have \U1B001 , which is Hiragana Archaic Ye, and JS strings treat this as a surrogate pair; how does that work?

Random.choice([String.fromCodePoint(0x1B001)])
Does this evaluate to '𛀁' or one of ('\uD82C' or '\uDC01') ?

That’s a good question. I’ve also since learned that properly splitting a string into an array of characters requires knowing the uesr’s locale (at least, it does if you use Intl.Segmenter with “graphene” granularity).

The way I see it, this Random.choice() API is primarily meant for arrays, but other data structures can be supported depending on how we decide to go about it. We could:

  • Make it accept any array-like value. This will automatically provide basic support for strings, but it support graphene clusters at all.
  • Make it accept any iterable, consuming the iterable into memory then picking out random items. Built-in iterables could be optimized so it doesn’t literally have to run through the iterable to start making random choices (unless the user has modified Symbol.iterator on the built-in type). This would provide better support for strings, as the string’s iterator does keep those surrogate pairs together. The support still won’t be as robust as Intl.Segmenter though (nor can it, without knowing which locale it should operate under).
  • Make it so Random.choice() only accepts arrays. If you want to use a different data type, you’d have to convert it to an array yourself first. This option feels the least JavaScript-y though.

One of the most common uses I have for Random.choice() with strings is to generate random ids, in which case I really only need ASCII support, so any of the above implementations would work for my use-cases. For being able to split arbitrary unicode strings into characters, any of the above fall short and Intl.Segmenter() should probably be used to first convert it to an array, in the proper locale.

You could have Random.choice accept an Array-like object in addition to Arrays, and have it shove the Array-like objects into Array.from() . That would solve the surrogate pair issue, because that function handles surrogate pairs properly. That'd also be more flexible.

That's true, that could be a good way to go