Created
August 16, 2022 18:26
-
-
Save cjbrooks12/cbe7e515ecfd75d63ca68db42da3a41b to your computer and use it in GitHub Desktop.
Kotlin Immutable Random
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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