SE-0202 Amendment Proposal: Rename Random to DefaultRandomNumberGenerator

Because the higher-level APIs take a generator inout (because in the case of other sources, it needs to mutate the source). You can't create a mutable instance and pass it as an inout argument in one shot (so couldn't use it as a default).

I think a global shared instance, even a stateless one, could lead to problems with memory exclusivity.

3 Likes

I'm just going to repeat what I said in the previous thread, showing why brevity in these names isn't important, as a few people have raised this concern here.

The default generator is supplied as a default argument to methods on types that produce random values. The only place this type should appear is in function signature definitions.

For example, this code in the standard library (exact implementation details elided):

  public func randomElement<T: RandomNumberGenerator>(
    using generator: inout T
  ) -> Element?
  
  public func randomElement() -> Element? {
    return randomElement(using: &Random.default)
  }
}

will become:

  public func randomElement<T: RandomNumberGenerator>(
    using generator: inout T
  ) -> Element?
  
  public func randomElement() -> Element? {
    return randomElement(using: &DefaultRandomNumberGenerator.shared)
  }
}

The end user never has to type the long name. A user wanting a random element from the collection write collection.randomElement(). That's it. The stdlib generator is rarely typed explicitly.

4 Likes

I’ve been missing from discussions recently and will have to circle back and catch up, but given that I participated in review of this proposal I will chime in here.

Like several others, I disagree (strongly) with the proposed amendment:

There is essentially nothing else besides the default RNG that can legitimately lay claim to the name “Random”—and even those who wish to use it as a namespace can declare extensions of Random with their custom type.

Nothing about the proposed amended name adds clarity to “Random”: as it is a well-established term of art, it is clearly a RNG, and since it’s unadorned, it really can’t be anything other than the default RNG. Moreover, the name “Random” communicates that we have attempted to design a type appropriate for general use, not just a placeholder type to be superseded by a custom superior implementation (as the word “default” may imply to some readers). I do not think a strong case can be made that “default” and “number generator” are needful words in this context, and therefore the principle of “omit needless words” comes into play here.

That the core team is having trouble renaming “Random.default” is, to me, a symptom of the redundancy of the proposed amended name, and the solution that I would propose is to address the underlying cause: do not adopt this proposed amendment.

(To Erica’s question: why not have Random() instead of Random.default? It is a common mistake to seed a new PRNG every time a random number is needed; for some PRNGs, this has some overhead, but crucially it is not sound in terms of obtaining good randomness. Although the default Random could conceivably be designed so that these are not issues, to make Random() become the preferred way of using a shared default PRNG would be actively educating users to misuse other PRNGs.)

18 Likes

Imho this is a restriction whose removal is worthy of discussion (not because of this specific example).

4 Likes

Another restriction that would be nice to remove is the inability to specify a default argument for a generic function parameter. Right now, one has to manually overload a generic function to give it a default argument. For instance, this does not compile:

public func die<Source: RandomNumberGenerator>(sides n: Int, using generator: inout Source = &Random.default) -> () -> Int {
    ...
}

Even though there is only one possible meaning for this declaration, and that meaning is perfectly reasonable.

This is largely off topic, though.

I looked over the original proposal again and agree. In that case, it does seem reasonable to prefer a descriptive and correct. name over a terse yet misleading name

+1 to DefaultRandomNumberGenerator.shared

-1 to this proposal. For the big reason that Xiaodi stated: nothing other than the default random number generator could possibly use the name Random. There is nothing misleading in the name, therefore this proposal solves a non-problem.

After re-reading Benjamin’s pitch twice, I can not find any argument that the name Random is misleading. It contains only argument that users should consider other sources of randomness than the default — which means they should consider rolling their own implementations of RandomNumberGenerator protocol, I’m guessing mostly some PRNGs. This is already served by all the random methods on numeric types and collections that take the inout using: argument.

Renaming the struct Random to the pitched horribly overlong name would preclude future extensions as described by @Alejandro here, as well as ruin a convenient semantically meaningful point to hang user’s custom extension off of (Random serving as namespace, completely in line with Alejandro’s vision).

I really don’t understand the motivation for this amendment at all. I can see it causing a lot of harm and can not find a single reason why. Can somebody spell it out for me, please? (@bzamayo, @Ben_Cohen)

5 Likes

Imho the whole discussion is too hypothetical, as there is no clear vision how RNGs should be used — if something is changed, there should be more than speculation (it shouldn't be that hard to write a proof of concept as it's not required to dive into the C++ layer).

What about all those auto-generated next methods?

let number: Int = DefaultRandomNumberGenerator.default.next(upperBound: 42)

doesn't look that nice...

1 Like

I think you're supposed to write that as:

let number = Int.random(in: 0 ... 42)

(The "auto-generated next methods" is a generic method only for unsigned fixed-width integer types, and is probably meant to only be used in more "lower level" use cases.)

4 Likes

But why should low level code look ugly? I really like long names, but some of the Swift std lib are beyond sane. Why is that?

1 Like

Exactly.

This thread appears to demonstrate that there is a clear misperception that Random.default.next() is something you're supposed to use. It isn't. Ever.

2 Likes

Let's not forget that the standard library sets the precendent for what third-party libraries will name their RNGs. If the name is DefaultRandomNumberGenerator, then we will see most libraries adopting the same naming convention and we will end up with names like MersenneTwisterRandomNumberGenerator, which we will have to type out fully.

I'd hate to have to type:

Int.random(in: 0...100, using: &MersenneTwisterRandomNumberGenerator.shared)

when it could have been as simple as:

Int.random(in: 0...100, using: &Random.mersenneTwister).

I also like the discoverability of having it all under a Random namespace, so I could just type Random. and get an autocompleted list of all the possible random algorithms I can use.

If the name must be changed, then I like @cal's idea of introducing a Random namespace so we can continue to use the short forms like Random.mersenneTwister.

15 Likes

That's a pretty compelling point in-favor of using a Random namespace.

I'm not sure why you think this case is rare. We want people to define random() methods on their types when they need to generate random instances, and we want them to make sure these methods can be parameterized with a RandomNumberGenerator instance for testing/orthogonality/dependency injection purposes. The fact that you must write a boilerplate overload is already expected to impede adoption of this pattern. We ought to care that we're making this boilerplate even wordier without an increase in clarity.

Here are a few different ways you could write a random() function if the language supported it, listed in the order I would prefer:

// 1
static func random<RNG: RandomNumberGenerator>(using rng: inout RNG = &.default) -> Self { … }

// 2
static func random<RNG: RandomNumberGenerator>(using rng: inout RNG = &Random.default) -> Self { … }

// 3
static func random<RNG: RandomNumberGenerator>(using rng: inout RNG = &DefaultRandom.shared) -> Self { … }

// 4
static func random<RNG: RandomNumberGenerator>(using rng: inout RNG = &DefaultRandomNumberGenerator.shared) -> Self { … }

// 5
static func random<RNG: RandomNumberGenerator>(using rng: inout RNG) -> Self { … }
static func random() -> Self { return random(using: &.default) }

// 6
static func random<RNG: RandomNumberGenerator>(using rng: inout RNG) -> Self { … }
static func random() -> Self { return random(using: &Random.default) }

// 7
static func random<RNG: RandomNumberGenerator>(using rng: inout RNG) -> Self { … }
static func random() -> Self { return random(using: &DefaultRandom.shared) }

// 8
static func random<RNG: RandomNumberGenerator>(using rng: inout RNG) -> Self { … }
static func random() -> Self { return random(using: &DefaultRandomNumberGenerator.shared) }

We are already way down the list from the ideal amount of boilerplate. I think it should bother us a lot that we're moving even further down.

(If we are going to do this, though, we should at least use shared as the property name. The instances have no identity, and they share a source of randomness, so shared is an accurate description which conveys explicitly something that was merely implied before. Using a name which is redundant or tautological, like default or instance, would just add insult to injury.)

2 Likes

Just to clarify for myself. This means that using next() on the Iterator of a 'normal' Sequence like this

var it = (0...9).makeIterator()

while let item = it.next() {
    // do something
}

is a completely legitimate pattern, but calling next() of a RandomGenerator shall only be implemented in an own RNG, but never be called outsite such an implementation?

1 Like

Has that been documented somewhere?

1 Like

I would like to see the type be called DefaultRandomNumberGenerator, but the shared instance be accessed via Random.default. Random would be an uninhabited enum or something.

2 Likes

Short is beautiful. Short code is better readable.

DefaultRandomNumberGenerator for something so trivial is outright crazy. The first thing I would drop is 'Default'. If there are more random number generators for specific purposes, add a prefix to them, i.e. SuperSecureAndCryptographicallyAlmostPerfectRandomNumberGenerator.

Without a prefix it's simply a general use RandomNumberGenerator, and that's it.

What does a random number generator typically generate? Yeah, numbers. So why not: RandomGenerator? (If you need random bitstreams or random strings, you can always define another class or an appropriate method name.)

I really really dislike the super long class and method names many Apple developers seem so happy with. The resulting code is so awkward.

Personally I like:

public enum RandomNumberGenerators {}
public extension RandomNumberGenerators {
    private struct Default: RandomNumberGenerator { ... }
    static defaultSharedBaseGenerator = Default()
}

Then I could write:

struct MyType {
    func random<R>(using rng: inout R = &RandomNumberGenerators.defaultSharedBaseGenerator) -> MyType where R: RandomNumberGenerator { ... }
}

Which I think:

  1. Reads well.
  2. Allows other generators too be added, to RandomNumberGenerators, in the future.
  3. Will discourage people from calling the generator directly.