-
Notifications
You must be signed in to change notification settings - Fork 4
Description
This is a continutation of #20
I would find it very unfortunate if the random APIs for picking integers go against the design of every other function in the language, and went with fully-open ranges instead of half-open ones.
(half-open would mean Random.int(0, 3) -> 0 or 1 or 2 and full-open would mean Random.int(0, 3) -> 0 or 1 or 2 or 3).
There's a couple of reasosn to use half-open APIs.
- When I program, I don't want to be required to remember which APIs are half-opened and which ones are full-open. I'd like to be able to just assume that everything is half-open.
- Programmers are very used to writing out logic using half-open APIs. It's natural for us. Not only that, half-open APIs tend to "lego" together very well without requiring off-by-one twiddling. I venture one of the most common uses of
Random.int()would be to pick a random element from an array (at least, until the language gets its own function to do it). Compare these solutions and notice how well the "half-open" version just legos into it:
// With half-open
items[Random.int(0, items.length)]
// With full-open
items[Random.int(0, items.length - 1)]Or, here's a slightly more complicated example - say we're given an array and an index, and we need to find a random element in the first half of the array (below the index) and another in the second half of the array (at or after the index).
// With half-open
function (items, index) {
return [
items[Random.int(0, index)],
items[Random.int(index, items.length)],
];
}
// With Full-open
function (items, index) {
return [
items[Random.int(0, index - 1),
items[Random.int(index, items.length - 1),
];
}By the way, notice how similar this example is to, say, an API where we need to partition an array into two halfs using a provided index.
function (items, index) {
return [
items.slice(0, index),
items.slice(index, items.length), // Or omit items.length, I'm keeping it here to show the simularities
];
}This goes back to my point that the rest of the language is half-opened - we're used to using .slice() in this sort of manner, and it would be nice if Random.int(), which has a very similar argument list didn't break our expectation in how it behaves.
Point is, any time you're dealing with random integers and arrays, you're going to be cursing the unnatrual full-openedness of it. This was a point that was already made in the previous thread I linked to, but I'm hoping to really express how damaging this is, as this is a very common use-case. In fact, I'd argue that more projects need to deal with picking random indicis vs picking random integers for other purposes.
Why was full-openedness picked?
I believe it mainly came from this argument.
If you're generating an int for a dice roll or similar, you often want a closed range - Random.int(1,6) to roll a die, for example, feels a lot more natural than Random.int(1, 7) (or Random.int(6)+1, for the langs that only offer a max).
This is true. But this is also true with any API in the language. Why should random integers be different?
You have a list of test scores sorted from best to worst, and you want to get the second item in the list. Well, you got to do scores[1]. Want the last item in the array? Before .at(-1) was an option, programmers were very used to doing scores[scores.length - 1]. And so on. Yes, Random.int(1, 7) is unnatural for picking a dice role, but we're already used to it. And there's ways to represent it so it's not so bad, such as const DICE_SIDES = 6; return 1 + random.int(0, DICE_SIDES);.
Middle ground?
If we're really worried about making it easy to pick random integers for dice rolls, perhaps we could find a way to support both half-opened and full-opened.
For example, an options bag to control opened-ness (This option seems appealing to me - I feel like most languages shy away from it because it's the exact same as doing a simple +1, but it's much more expressive and easier to understand to use "inclusive" instead of twiddling with off-by-one calculations).
Random.int(0, 3) -> 0 or 1 or 2
Random.int(0, 3, { inclusive: true }) -> 0 or 1 or 2 or 3Another path would be to provide two functions. I'm a little less fond of this one though - I still don't love having a function out there with the default behavior of full-openedness, but it's an improvement:
Random.index(0, 3) -> 0 or 1 or 2
Random.int(0, 3) -> 0 or 1 or 2 or 3Or, we just ditch full-openedness and let programmers deal with the off-by-one issues when rolling dice, like we're already used to doing.