Skip to content

Commit

Permalink
Add a bit of documentation / formatting to Random (#187)
Browse files Browse the repository at this point in the history
Co-authored-by: Mareks Rampāns <8796159+mr-git@users.noreply.github.com>
  • Loading branch information
rtar and mr-git authored Feb 28, 2025
1 parent 9a22021 commit 4427960
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 19 deletions.
2 changes: 2 additions & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ addSbtPlugin("com.evolution" % "sbt-artifactory-plugin" % "0.0.2")
addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.0")

addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4")

addSbtPlugin("com.thoughtworks.sbt-api-mappings" % "sbt-api-mappings" % "3.0.2")
82 changes: 63 additions & 19 deletions src/main/scala-2/com/evolutiongaming/random/Random.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.evolutiongaming.random

import cats.data.StateT
import cats.effect.Clock
import cats.implicits._
import cats.{FlatMap, Id, ~>}
Expand All @@ -18,6 +19,11 @@ trait Random[F[_]] {

object Random {

/** The type used as a seed for the random number generator.
*
* In this library it also used as an internal state of the random number
* generator.
*/
type Seed = Long

def apply[F[_]](implicit F: Random[F]): Random[F] = F
Expand All @@ -36,62 +42,95 @@ object Random {
}
}

type SeedT[A] = cats.data.StateT[Id, Seed, A]
/** The pseudo random number generator (PRNG) for a single specific type `A`
* based on
* [[https://en.wikipedia.org/wiki/Linear_congruential_generator LCG]]
* algorithm.
*
* It takes some `state1` as an input and returns a new `state2` and a random
* value of type `A`.
*
* Technically, it is just a function from `(Seed)` to `(Seed, A)`.
*
* `StateT` is used instead of a plain function, it has the ability to chain
* several calls in for comprehensions, instead of doing something like
* following:
* ```
* val (state1, a) = f(seed)
* val (state2, b) = g(state1)
* val (state3, c) = h(state2)
* ```
*
* The practice shown that this introduces a lot of confusion, so in future
* library versions `StateT` will not be exposed in public API.
*/
type SeedT[A] = StateT[Id, Seed, A]

object SeedT {

/** Set of random number generators for common numeric types */
val Random: Random[SeedT] = {

val doubleUnit = 1.0 / (1L << 53)

val floatUnit = (1 << 24).toFloat

def next(bits: Int): SeedT[Int] = {
def next(bits: Int): SeedT[Int] =
SeedT { seed =>
val r = (seed >>> (48 - bits)).toInt
val s1 = (seed * 0x5deece66dL + 0xbL) & ((1L << 48) - 1)
(s1, r)
}
}

new Random[SeedT] {

def int = next(32)
def int: SeedT[Int] = next(32)

def long = {
def long: SeedT[Long] =
for {
a0 <- next(32)
a1 = a0.toLong << 32
a2 <- next(32)
} yield {
a1 + a2
}
}

def float = {
def float: SeedT[Float] =
for {
a <- next(24)
} yield {
a / floatUnit
}
}

def double = {
def double: SeedT[Double] =
for {
a0 <- next(26)
a1 = a0.toLong << 27
a2 <- next(27)
} yield {
(a1 + a2) * doubleUnit
}
}

}
}

def apply[A](f: Seed => (Seed, A)): SeedT[A] =
cats.data.StateT[Id, Seed, A] { seed => f(seed) }
StateT[Id, Seed, A] { seed => f(seed) }
}

/** Snapshot of a state of a stateful random number generator.
*
* @param seed
* The internal state of the random number generator that will be used to
* generate the next random number. The initial `seed` is quite important
* as having `0` as seed reduces this LCG PRNG to lesser Lehmer RNG.
* Consider using [[State#fromClock]] for a good initial seed.
* @param random
* The stateless part of the random number generator, i.e. the set of
* functions from `state1` to `(state2, A)`, where `A` is the type of
* outputs of a random number generator such as `Int`, `Long`, `Float`, or
* `Double`.
*/
final case class State(seed: Seed, random: Random[SeedT] = SeedT.Random)
extends Random[State.Type] {

Expand All @@ -100,29 +139,34 @@ object Random {
(State(seed1, random), a)
}

def int = apply(random.int)
def int: (State, Int) = apply(random.int)

def long = apply(random.long)
def long: (State, Long) = apply(random.long)

def float = apply(random.float)
def float: (State, Float) = apply(random.float)

def double = apply(random.double)
def double: (State, Double) = apply(random.double)
}

object State { self =>
object State {

type Type[A] = (State, A)

/** Create an instance of [[Random.State]] based on [[cats.effect.Clock]].
*
* The state is initialized with a constant seed known to be good and mixed
* together with a current time to add an additional randomness.
*/
def fromClock[F[_]: Clock: FlatMap](
random: Random[SeedT] = SeedT.Random
): F[State] = {
): F[State] =
for {
nanos <- Clock[F].nanos
} yield {
val seed =
(nanos ^ 3447679086515839964L ^ 0x5deece66dL) & ((1L << 48) - 1)
apply(seed, random)
State(seed, random)
}
}

}
}

0 comments on commit 4427960

Please sign in to comment.