Skip to content

Instantly share code, notes, and snippets.

@cjbrooks12
Created August 16, 2022 18:26
Show Gist options
  • Save cjbrooks12/cbe7e515ecfd75d63ca68db42da3a41b to your computer and use it in GitHub Desktop.
Save cjbrooks12/cbe7e515ecfd75d63ca68db42da3a41b to your computer and use it in GitHub Desktop.
Kotlin Immutable Random
import kotlin.random.Random
/**
* Denotes an instance of [Random] that does not mutate its own internal state. Unlike standard [Random],
* [ImmutableRandom] will always return the same value for subsequent calls to [nextBits], [nextInt], etc. One must
* call [nextRandom] to get a new instance of [ImmutableRandom] with a state that would typically have been updated
* internally.
*
* For the same seed, any sequence of `next*(), nextRandom()` should yield the same result as a normal [Random] calling
* the same sequence of `next*
*/
abstract class ImmutableRandom : Random() {
abstract fun nextRandom(): ImmutableRandom
}
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
class TestImmutableRandom {
@Test
fun testActuallyImmutable() {
val standardRandom = Random(0)
val immutableRandom = ImmutableRandom(0)
assertEquals(-1934310868, standardRandom.nextInt())
assertEquals(1409199696, standardRandom.nextInt())
assertEquals(-649160781, standardRandom.nextInt())
assertEquals(-1454478562, standardRandom.nextInt())
assertEquals(-1934310868, immutableRandom.nextInt())
assertEquals(-1934310868, immutableRandom.nextInt())
assertEquals(-1934310868, immutableRandom.nextInt())
assertEquals(-1934310868, immutableRandom.nextInt())
}
@Test
fun testSameSequenceAsStandardRandom() {
val standardRandom = Random(0)
val immutableRandom = ImmutableRandom(0)
val standardRandomSequence: List<Int> = (0..1000).map {
standardRandom.nextInt()
}
val immutableRandomSequence: List<Int> = (0..1000)
.runningFold(immutableRandom to 0) { acc, next -> acc.first.nextRandom() to acc.first.nextInt() }
.map { it.second }
.drop(1)
assertEquals(standardRandomSequence, immutableRandomSequence)
}
}
/**
* Random number generator, using Marsaglia's "xorwow" algorithm
*
* Cycles after 2^192 - 2^32 repetitions.
*
* For more details, see Marsaglia, George (July 2003). "Xorshift RNGs". Journal of Statistical Software. 8 (14). doi:10.18637/jss.v008.i14
*
* Available at https://www.jstatsoft.org/v08/i14/paper
*
* This is an exact copy of the default [kotlin.random.XorWowRandom], but where its internal state is immutable. One
* must call [ImmutableRandom.nextRandom] to get an instance that will return the next step in the sequence.
*
*/
internal class XorWowImmutableRandom internal constructor(
internal val _x: Int,
internal val _y: Int,
internal val _z: Int,
internal val _w: Int,
internal val _v: Int,
internal val _addend: Int
) : ImmutableRandom() {
override fun nextInt(): Int {
// Equivalent to the xorxow algorithm
// From Marsaglia, G. 2003. Xorshift RNGs. J. Statis. Soft. 8, 14, p. 5
var t = _x
t = t xor (t ushr 2)
val x = _y
val y = _z
val z = _w
val v0 = _v
val w = v0
t = (t xor (t shl 1)) xor v0 xor (v0 shl 4)
val v = t
val addend = _addend + 362437
return t + addend
}
override fun nextBits(bitCount: Int): Int =
nextInt().takeUpperBits(bitCount)
override fun nextRandom(): XorWowImmutableRandom {
// Equivalent to the xorxow algorithm
// From Marsaglia, G. 2003. Xorshift RNGs. J. Statis. Soft. 8, 14, p. 5
var t = _x
t = t xor (t ushr 2)
val x = _y
val y = _z
val z = _w
val v0 = _v
val w = v0
t = (t xor (t shl 1)) xor v0 xor (v0 shl 4)
val v = t
val addend = _addend + 362437
return XorWowImmutableRandom(
_x = x,
_y = y,
_z = z,
_w = w,
_v = v,
_addend = addend,
)
}
}
internal fun Int.takeUpperBits(bitCount: Int): Int =
this.ushr(32 - bitCount) and (-bitCount).shr(31)
public fun ImmutableRandom(seed: Int): ImmutableRandom = ImmutableRandom(seed, seed.shr(31))
public fun ImmutableRandom(seed: Long): ImmutableRandom = ImmutableRandom(seed.toInt(), seed.shr(32).toInt())
public fun ImmutableRandom(seed1: Int, seed2: Int): ImmutableRandom {
var initialInstance = XorWowImmutableRandom(seed1, seed2, 0, 0, seed1.inv(), (seed1 shl 10) xor (seed2 ushr 4))
with(initialInstance) {
require((_x or _y or _z or _w or _v) != 0) { "Initial state must have at least one non-zero element." }
}
// some trivial seeds can produce several values with zeroes in upper bits, so we discard first 64
repeat(64) { initialInstance = initialInstance.nextRandom() }
return initialInstance
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment