diff --git a/project/plugins.sbt b/project/plugins.sbt index 58af23b..f482f46 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -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") diff --git a/src/main/scala-2/com/evolutiongaming/random/Random.scala b/src/main/scala-2/com/evolutiongaming/random/Random.scala index 6902dfc..f59d954 100644 --- a/src/main/scala-2/com/evolutiongaming/random/Random.scala +++ b/src/main/scala-2/com/evolutiongaming/random/Random.scala @@ -1,5 +1,6 @@ package com.evolutiongaming.random +import cats.data.StateT import cats.effect.Clock import cats.implicits._ import cats.{FlatMap, Id, ~>} @@ -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 @@ -36,29 +42,51 @@ 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 @@ -66,17 +94,15 @@ object Random { } 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 @@ -84,14 +110,27 @@ object Random { } 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] { @@ -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) } - } + } }