28

I'm trying to generate seeded random numbers with Swift 4.2+, with the Int.random() function, however there is no given implementation that allows for the random number generator to be seeded. As far as I can tell, the only way to do this is to create a new random number generator that conforms to the RandomNumberGenerator protocol. Does anyone have a recommendation for a better way to do it, or an implementation of a RandomNumberGenerator conforming class that has the functionality of being seeded, and how to implement it?

Also, I have seen two functions srand and drand mentioned a couple times while I was looking for a solution to this, but judging by how rarely it was mentioned, I'm not sure if using it is bad convention, and I also can't find any documentation on them.

I'm looking for the simplest solution, not necessarily the most secure or fastest performance one (e.g. using an external library would not be ideal).

Update: By "seeded", I mean that I was to pass in a seed to the random number generator so that if I pass in the same seed to two different devices or at two different times, the generator will produce the same numbers. The purpose is that I'm randomly generating data for an app, and rather than save all that data to a database, I want to save the seed and regenerate the data with that seed every time the user loads the app.

RPatel99
  • 7,448
  • 2
  • 37
  • 45
  • Maybe it would help if you told us what it is you want to accomplish, the word seed/seeded seems to be interpreted quite differently on the net. – Joakim Danielson Feb 22 '19 at 07:12
  • If you use the SystemRandomNumberGenerator, seeding is done automagically for you: https://developer.apple.com/documentation/swift/systemrandomnumbergenerator – Andreas Oetjen Feb 22 '19 at 07:15
  • 2
    Do you have to use `RandomNumberGenerator`? GamePlayKit has various random number generators, see for example https://stackoverflow.com/a/53355215. – Martin R Feb 22 '19 at 07:59
  • @JoakimDanielson I updated the question – RPatel99 Feb 22 '19 at 16:42
  • @AndreasOetjen I need to be able to pass in the seed on my own terms, see the update in the question. – RPatel99 Feb 22 '19 at 16:43
  • @MartinR I'd rather not import any packages, but if there's no other option then I'll definitely look into GamePlayKit. – RPatel99 Feb 22 '19 at 17:14
  • +1 on Martin R's suggestion to use a GamePlaykit random number generator. They have deterministic generators to allow the generated numbers to be reproducible. MartinR should upgrade that comment to an answer. I remember a WWDC video on gameplaykit mentioning that functionality. – Darrell Root Feb 22 '19 at 22:09
  • Possible duplicate of [Is there're a way to seed Swift 4.2 random number generator](https://stackoverflow.com/questions/53348370/is-therere-a-way-to-seed-swift-4-2-random-number-generator) – pjs Feb 23 '19 at 22:32

6 Answers6

13

So I used Martin R's suggestion to use GamePlayKit's GKMersenneTwisterRandomSource to make a class that conformed to the RandomNumberGenerator protocol, which I was able to use an instance of in functions like Int.random():

import GameplayKit

class SeededGenerator: RandomNumberGenerator {
    let seed: UInt64
    private let generator: GKMersenneTwisterRandomSource
    convenience init() {
        self.init(seed: 0)
    }
    init(seed: UInt64) {
        self.seed = seed
        generator = GKMersenneTwisterRandomSource(seed: seed)
    }
    func next<T>(upperBound: T) -> T where T : FixedWidthInteger, T : UnsignedInteger {
        return T(abs(generator.nextInt(upperBound: Int(upperBound))))
    }
    func next<T>() -> T where T : FixedWidthInteger, T : UnsignedInteger {
        return T(abs(generator.nextInt()))
    }
}

Usage:

// Make a random seed and store in a database
let seed = UInt64.random(in: UInt64.min ... UInt64.max)
var generator = Generator(seed: seed)
// Or if you just need the seeding ability for testing,
// var generator = Generator()
// uses a default seed of 0

let chars = ['a','b','c','d','e','f']
let randomChar = chars.randomElement(using: &generator)
let randomInt = Int.random(in: 0 ..< 1000, using: &generator)
// etc.

This gave me the flexibility and easy implementation that I needed by combining the seeding functionality of GKMersenneTwisterRandomSource and the simplicity of the standard library's random functions (like .randomElement() for arrays and .random() for Int, Bool, Double, etc.)

RPatel99
  • 7,448
  • 2
  • 37
  • 45
  • 1
    I wonder if there's a problem with the range of the values produced with the above generator, given that GKRandom (and hence GKMersenneTwisterRandomSource) is documented to produce values in [INT32_MIN, INT32_MAX] range. Please see below for an alternative variant accounting that. – Grigory Entin Aug 06 '19 at 07:27
  • What @GrigoryEntin said. Even on 64-bit platforms, using `abs()` limits you to half the range, so for instance `next()` will never return anything in the range `(Int64.max + 1)...(UInt64.max)`. And I suspect that on 64-bit platforms `next()` will produce a lot of overflow errors. – David Moles Apr 08 '20 at 22:10
  • Strange. I tried this code and I get an infinite loop if I try something like `Double.random(in: 0...1, using: &gen)`. Happens on my iPhone and my Mac. It's calling `next` repeatedly but never returning to give the random Double. – Rob N Apr 20 '20 at 03:28
  • @RobN, did tou solve the issue? I am fasing the same issue – Tobias Oct 09 '20 at 13:28
  • 1
    @Tobias It's been a while, but yes I think so. Try something like Grigory Entin's answer, where you override only the `next() -> UInt64` method of the protocol. I'm not sure why this answer is trying to override the generic extension methods. – Rob N Oct 09 '20 at 13:49
13

Here's alternative to the answer from RPatel99 that accounts GKRandom values range.

import GameKit

struct ArbitraryRandomNumberGenerator : RandomNumberGenerator {

    mutating func next() -> UInt64 {
        // GKRandom produces values in [INT32_MIN, INT32_MAX] range; hence we need two numbers to produce 64-bit value.
        let next1 = UInt64(bitPattern: Int64(gkrandom.nextInt()))
        let next2 = UInt64(bitPattern: Int64(gkrandom.nextInt()))
        return next1 ^ (next2 << 32)
    }

    init(seed: UInt64) {
        self.gkrandom = GKMersenneTwisterRandomSource(seed: seed)
    }

    private let gkrandom: GKRandom
}
Grigory Entin
  • 1,617
  • 18
  • 20
  • 1
    I tried this, but `next(upperBound:)` gave me weird distributions until I masked `next1` and `next2` with `& 0xFFFF_FFFF` to keep just the least significant bits. – David Moles Apr 08 '20 at 22:01
  • 1
    (I guess `GKMersenneTwisterRandomSource` returns the full signed `Int64` range on 64-bit platforms?) – David Moles Apr 08 '20 at 22:06
  • 1
    @DavidMoles I checked it with GKMersenneTwisterRandomSource on macOS. It does not give me anything above 0x7FFF_FFFF (accounting the sign), so it doesn't look like it's 64-bit. As for masking with 0xFFFF_FFFF - I believe that indeed makes sense, because "half of the time" next1 would be generated from a negative Int32 and that means that it would have all high 32 bits set, and `next1 | (next2 << 32)` would not change those high bits (i.e. the generated number would have 0xFFFF_FFFF at the high 32-bit). An alternative would be probably using 'xor' instead of 'or' in `next1 ^ (next2 << 32)`. – Grigory Entin Apr 10 '20 at 12:47
  • 4
    @DavidMoles I checked it with small test, yes, half of the time I see 0xffffffff in the upper part with `next1 | (next2 << 32)`. With `next1 ^ (next2 << 32)` it looks random. Going to update the answer, thanks! – Grigory Entin Apr 10 '20 at 12:59
7

Simplified version for Swift 5:

struct RandomNumberGeneratorWithSeed: RandomNumberGenerator {
    init(seed: Int) { srand48(seed) }
    func next() -> UInt64 { return UInt64(drand48() * Double(UInt64.max)) }
}
@State var seededGenerator = RandomNumberGeneratorWithSeed(seed: 123)
// use fixed seed for testing, when deployed use Int.random(in: 0..<Int.max)

Then to use it:

let rand0to99 = Int.random(in: 0..<100, using: &seededGenerator)
mrzzmr
  • 695
  • 1
  • 7
  • 6
2

Looks like Swift's implementation of RandomNumberGenerator.next(using:) changed in 2019. This affects Collection.randomElement(using:) and causes it to always return the first element if your generator's next()->UInt64 implementation doesn't produce values uniformly across the domain of UInt64. The GKRandom solution provided here is therefore problematic because it's next->Int method states:

     * The value is in the range of [INT32_MIN, INT32_MAX].

Here's a solution that works for me using the RNG in Swift's TensorFlow found here:


public struct ARC4RandomNumberGenerator: RandomNumberGenerator {
  var state: [UInt8] = Array(0...255)
  var iPos: UInt8 = 0
  var jPos: UInt8 = 0

  /// Initialize ARC4RandomNumberGenerator using an array of UInt8. The array
  /// must have length between 1 and 256 inclusive.
  public init(seed: [UInt8]) {
    precondition(seed.count > 0, "Length of seed must be positive")
    precondition(seed.count <= 256, "Length of seed must be at most 256")
    var j: UInt8 = 0
    for i: UInt8 in 0...255 {
      j &+= S(i) &+ seed[Int(i) % seed.count]
      swapAt(i, j)
    }
  }

  // Produce the next random UInt64 from the stream, and advance the internal
  // state.
  public mutating func next() -> UInt64 {
    var result: UInt64 = 0
    for _ in 0..<UInt64.bitWidth / UInt8.bitWidth {
      result <<= UInt8.bitWidth
      result += UInt64(nextByte())
    }
    print(result)
    return result
  }

  // Helper to access the state.
  private func S(_ index: UInt8) -> UInt8 {
    return state[Int(index)]
  }

  // Helper to swap elements of the state.
  private mutating func swapAt(_ i: UInt8, _ j: UInt8) {
    state.swapAt(Int(i), Int(j))
  }

  // Generates the next byte in the keystream.
  private mutating func nextByte() -> UInt8 {
    iPos &+= 1
    jPos &+= S(iPos)
    swapAt(iPos, jPos)
    return S(S(iPos) &+ S(jPos))
  }
}

Hat tip to my coworkers Samuel, Noah, and Stephen who helped me get to the bottom of this.

Clay Garrett
  • 1,023
  • 8
  • 11
1

I ended up using srand48() and drand48() to generate a pseudo-random number with a seed for a specific test.

class SeededRandomNumberGenerator : RandomNumberGenerator {

    let range: ClosedRange<Double> = Double(UInt64.min) ... Double(UInt64.max)

    init(seed: Int) {
        // srand48() — Pseudo-random number initializer
        srand48(seed)
    }

    func next() -> UInt64 {
        // drand48() — Pseudo-random number generator
        return UInt64(range.lowerBound + (range.upperBound - range.lowerBound) * drand48())
    }
    
}

So, in production the implementation uses the SystemRandomNumberGenerator but in the test suite it uses the SeededRandomNumberGenerator.

Example:

let messageFixtures: [Any] = [
    "a string",
    ["some", ["values": 456]],
]

var seededRandomNumberGenerator = SeededRandomNumberGenerator(seed: 13)

func randomMessageData() -> Any {
    return messageFixtures.randomElement(using: &seededRandomNumberGenerator)!
}

// Always return the same element in the same order
randomMessageData() //"a string"
randomMessageData() //["some", ["values": 456]]
randomMessageData() //["some", ["values": 456]]
randomMessageData() //["some", ["values": 456]]
randomMessageData() //"a string"
ricardopereira
  • 11,118
  • 5
  • 63
  • 81
0

The srand48 implementations didn't work for me when I tried them with Bool.random(using:). They produced:

var randomNumberGenerator = RandomNumberGeneratorWithSeed(seed:69)
for _ in 0..<100 {
  print("\(Bool.random(using: &randomNumberGenerator))")
}
true
true
false
false
true
true
false
false
true
true
...

However, in the Swift Forums, I found a post from Nate Cook with a Swift implementation of a public domain algorithm that appears more random in my test above (no obvious pattern exists)

// This is a fixed-increment version of Java 8's SplittableRandom generator.
// It is a very fast generator passing BigCrush, with 64 bits of state.
// See http://dx.doi.org/10.1145/2714064.2660195 and
// http://docs.oracle.com/javase/8/docs/api/java/util/SplittableRandom.html
//
// Derived from public domain C implementation by Sebastiano Vigna
// See http://xoshiro.di.unimi.it/splitmix64.c
public struct SplitMix64: RandomNumberGenerator {
    private var state: UInt64

    public init(seed: UInt64) {
        self.state = seed
    }

    public mutating func next() -> UInt64 {
        self.state &+= 0x9e3779b97f4a7c15
        var z: UInt64 = self.state
        z = (z ^ (z &>> 30)) &* 0xbf58476d1ce4e5b9
        z = (z ^ (z &>> 27)) &* 0x94d049bb133111eb
        return z ^ (z &>> 31)
    }
}
Heath Borders
  • 30,998
  • 16
  • 147
  • 256