Improving the Random API

This thread is meant as a more general discussion of how the Random API could be improved, following up on threads like this (about a an issue that arises from the current API design) and this (about improving the methods for generating uniform floating point numbers as well as unsigned integers below a given upper bound).

As an initial suggestion I'd like to propose a simplification of the API that would solve the problem described in the first thread and make it at least a little more straight forward to write the improvements mentioned in the second.

I suggest we keep the core of the RandomNumberGenerator protocol as it is today, ie:

public protocol RandomNumberGenerator {
  /// Returns a value from a uniform, independent distribution of binary data.
  ///
  /// Use this method when you need random binary data to generate another
  /// value. If you need an integer value within a specific range, use the
  /// static `random(in:using:)` method on that integer type instead of this
  /// method.
  ///
  /// - Returns: An unsigned 64-bit random value.
  mutating func next() -> UInt64
}

But that (over time) we abandon
next() -> T,
next(upperBound:) -> T and
T._random()
as they can all be replaced by this single method:

extension RandomNumberGenerator {
  /// Returns a random value that is less than or equal to the given value.
  ///
  /// Use this method when you need random binary data to generate another
  /// value. If you need an integer value within a specific range, use the
  /// static `random(in:using:)` method on that integer type instead of this
  /// method.
  ///
  /// - Parameter maxValue: The greatest value that will be generated.
  /// - Returns: A random value of `T` in the range `0...maxValue`. Every
  ///   value in the range `0...maxValue` is equally likely to be returned.
  @inlinable
  public mutating func next<T: FixedWidthInteger & UnsignedInteger>(
    lessThanOrEqualTo maxValue: T = T.max
  ) -> T {
    // NOTE: Any value is a valid `maxValue` so no preconditions needed.
    if maxValue == 0 { return 0 }
    if T.bitWidth <= UInt64.bitWidth {
      if maxValue == T.max {
        return T(truncatingIfNeeded: self.next())
        // Or, we might implement some buffering here in order to not
        // throw away eg 56 bits for each call when T is UInt8.
      }
      // Code for the case when T.bitWidth <= 64 and maxValue < T.max,
      // ideally it would use the random bits as efficiently as possible
      // (ie only one bit per call is consumed if maxValue == 1 no
      // matter the type of T.
      // ...
    }
    // Code for the case when T.bitWidth > 64.
    // ...
  }
}

The buffering thing is only mentioned as an idea, and is of secondary concern, as it would require stored properties ( bufferedBits: UInt64 and bufferedBitCount ) either as requirements of the protocol or via an additional type
( BufferedRandomNumberGenerator<Base: RandomNumberGenerator> : RandomNumberGenerator { … } )
... and I guess it might not be worth its weight.

What do you think, would it make sense to introduce this method (sans the buffering) and phase out the ones it replaces?

2 Likes

I would still like to address the difficulty in producing large numbers of random bytes as discussed in Pitch: Requesting larger amounts of randomness from SystemRandomNumberGenerator.

2 Likes

Yes, it would be great if it could have efficient support for requesting anywhere from one or more single bits (consuming no more than the requested number of bits (from an internal buffer)) to large amounts of random data (written to a given pointer or appended to mutable collection or returned as an array).