(Edit: Yes, assuming that the PRNG implementation can provide custom implementations of the current _fill(bytes: )
method and any similar methods that transform the generator's output, and that might be subject to change. This is not currently the case, but as @benrimmington noted, it might be the case in the future for fill(bytes: ), but there are also other ones like the BinaryFloatingPoint.random(in:using:)
)
No, I'm of course assuming that the PRNG we are talking about is not somehow incorrectly implemented, ie I'm assuming that it's output is repeatable, and completely determined by a given seed, which is true for all PRNGs. I have already explained what I mean above, but here is an example program to make it clearer:
// For the purpose of this demonstration, this will just generate a sequence
// of bytes in increasing order (wrapping back to zero after 255). Not very
// random, I know, but it's only to make it easier to see my point, it could
// of course be any PRNG.
struct MyPrng : RandomNumberGenerator {
var currentByteValue = 0 as UInt8
mutating func next() -> UInt64 {
var ui64 = 0 as UInt64
for i in 0 ..< 8 {
ui64 |= UInt64(truncatingIfNeeded: currentByteValue) &<< (i * 8)
currentByteValue &+= 1
}
return ui64
}
}
var prng = MyPrng()
let demoActualBytesGenerated = false
if demoActualBytesGenerated {
// This will print the first eight bytes actually generated by this prng:
var firstUInt64 = prng.next()
withUnsafeBytes(of: &firstUInt64) { (byteBuf) -> Void in
let bytes = byteBuf.map { $0 }
print(bytes) // [0, 1, 2, 3, 4, 5, 6, 7]
}
} else {
// If the implementation used every generated byte, the following would
// print [0, 1, 2, 3, 4, 5, 6, 7] but since the current implementation
// generates a new UInt64 (8 bytes) for each requested byte, using only
// the first and throwing away the remaining 7 bytes, it will print
// [0, 8, 16, 24, 32, 40, 48, 56]:
var bytes = [UInt8]()
for _ in 0 ..< 8 {
bytes.append(UInt8.random(in: 0 ... UInt8.max, using: &prng))
}
print(bytes) // [0, 8, 16, 24, 32, 40, 48, 56]
}
So, the point is that if the Random API implementation should change (so that it used every byte instead of throwing some away), then (using the exact same PRNG, seeded with the exact same seed) I would suddenly get [0, 1, 2, 3, 4, 5, 6, 7] instead of the [0, 8, 16, 24, 32, 40, 48, 56] which I get with the current implementation.
A practical example where this is relevant:
Someone could have written a game with procedurally generated levels, so that each level is described using only a PRNG seed value (the developer has examined millions of levels/seeds, and selected some good ones, and arranged them in order of increasing difficulty). Now, if the Random API implementation changed, the levels would be completely different, even though the code of the game (including the seeds for the levels) has not changed.
This is of course only an issue if the game uses methods like:
UIntX.random(in: using:)
Float.random(in: using: )
...
Ie, any method that ends up calling:
extension RandomNumberGenerator {
public mutating func _fill(bytes buffer: UnsafeMutableRawBufferPointer) {
...
}
}
Or some similar Random API which transforms the generated raw bits into some value of some type, and whose implementation might change.
But since avoiding all such methods essentially means avoiding the whole Random API, I think this is a very relevant question:
Note that I'm all for changing stuff whenever it needs to, as long as people know that this is the case (and that their eg PRNG-based procedurally generated levels might change with any new version of Swift). Knowing this, it would probably be wise for them to roll their own little separate random API in order to have full control over these things.
The general advice would be: If you depend on repeatable results using your PRNG, don't implement it as part of Swift's Random API, roll your own on the side.