diff --git a/docs/std/random.md b/docs/std/random.md index f00ab03be4..2a33a39898 100644 --- a/docs/std/random.md +++ b/docs/std/random.md @@ -36,23 +36,75 @@ on the JVM: - `java.util.Random` - `java.security.SecureRandom` -## Creating a `Random` instance +## Creating and using a `Random` instance -Obtaining an instance of `Random` can be as simple as: +The following example shows the usage of `Random` in a way that is *referentially transparent*, meaning that you can know from the 'outside' what the result will be by knowing the inputs: ```scala mdoc:silent -import cats.effect.IO import cats.effect.std.Random +import cats.syntax.all._ +import cats.Monad +import cats.effect.unsafe.implicits.global + +object BusinessLogic { + + // use the standard implementation of Random backed by java.util.Random() + // (the same implementation as Random.javaUtilRandom(43)) + val randomizer: IO[Random[IO]] = Random.scalaUtilRandom[IO] + + // other possible implementations you could choose + val sr = SecureRandom.javaSecuritySecureRandom(3) // backed java.security.SecureRandom() + val jr = Random.javaUtilRandom(new java.util.Random()) // pass in the backing randomizer + + // calling .unsafeRunSync() in business logic is an anti-patten. + // Doing it here to make the example easy to follow. + def unsafeGetMessage: String = + Magic + .getMagicNumber[IO](5, randomizer) + .unsafeRunSync() +} -Random.scalaUtilRandom[IO] +object Magic { + def getMagicNumber[F[_] : Monad]( + mult: Int, + randomizer: F[Random[F]] + ): F[String] = + for { + rand <- randomizer.flatMap(random => random.betweenInt(1, 11)) // 11 is excluded + number = rand * mult + msg = s"the magic number is: $number" + } yield msg +} ``` -## Using `Random` -```scala mdoc -import cats.Functor -import cats.syntax.functor._ +Since `getMagicNumber` is not dependent on a particular implementation (it's referentially transparent), you can give it another instance of the type class as you see fit. + +This is particularly useful when testing. In the following example, we need our `Random` implementation to give back a stable value so we can ensure everything else works correctly, and our test assert succeeds. Since `randomizer` is passed into `getMagicNumber`, we can swap it out in our test with a `Random` of which we can make stable. In our test implementation, calls to `betweenInt` will *always* give back `7`. This stability of "randomness" allows us to test that our function `getMagicNumber` does what we intend: -def dieRoll[F[_]: Functor: Random]: F[Int] = - Random[F].betweenInt(0, 6).map(_ + 1) // `6` is excluded from the range +```scala mdoc:silent +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.must.Matchers._ + +class MagicSpec extends AnyFunSuite { + + // for testing, create a Random instance that gives back the same number every time. With + // this version of the Random type class, we can test our business logic works as intended. + implicit val r: IO[Random[IO]] = IO.pure( + new Random[IO] { + def betweenInt(minInclusive: Int, maxExclusive: Int): IO[Int] = + IO.pure(7) // gives back 7 every call + + // all other methods not implemented since they won't be called in our test + def betweenDouble(minInclusive: Double, maxExclusive: Double): IO[Double] = ??? + def betweenFloat(minInclusive: Float, maxExclusive: Float): IO[Float] = ??? + // ... snip: cutting out other method implementations for brevity + } + ) + + test("getMagicNumber text matches expectations") { + val result = MagicSpec.getMagicNumber[IO](5) + result.mustBe("the magic number is: 35") + } +} ``` ## Derivation