The generation of (pseudo-)random numbers is a perfect example for the differences between imperative (object-oriented) and purely functional programming. Scala’s support for both of these programming paradigms means that we can translate between them. In this post we start with the imperative random number generation from the Scala standard library, wrap it up in a purely functional random number generator, and translate that one back to an imperative one.

Imperative random number generation

The scala standard library provides a random number generator in a class Random, offering methods like nextInt:

val rng = new scala.util.Random(seed = 42)
println(rng.nextInt); // -1170105035 
println(rng.nextInt); // 234785527
println(rng.nextInt); // -1360544799

As you see, subsequent calls to nextInt on a Random object produce different results. Therefore, the expression rng.nextInt is neither deterministic nor referentially transparent - we could not replace all occurrences of that expression with a memoized value. This is because calling nextInt has side effects - it mutates the internal state of the object rng.

Whereas this is the idiomatic way to express random number generation in imperative programming, purely functional programming avoids side effects and mutation in favor of determinism and referential transparency. You may now wonder whether it is possible to use an imperative random number generator like scala.util.Random in a purely functional program at all. The answer is yes.

Purely functional random number generation

A purely functional random number generator can be expressed in Scala as follows:

trait RNG {
    def nextInt: (RNG, Int)
}
val rng: RNG = mkRNG(seed = 42) // initialize with seed = 42
println(rng.nextInt); // (RNG(seed = 13), -1170105035) 
println(rng.nextInt); // (RNG(seed = 13), -1170105035) 
println(rng.nextInt); // (RNG(seed = 13), -1170105035) 

Now RNG objects are immutable and generating a random number returns a changed RNG object instead of mutating r. The expression rng.nextInt is deterministic and referentially transparent. Next we look into an implementation of the RNG trait that wraps an imperative random number generator.

Wrapping an imperative random number generator in a purely functional random number generator

The fundamental idea of the RNG trait that it contains some immutable state that allows generating a deterministic random number stream. One way to do this is to use the last generated number as the state and some mathemagical formula to produce the next state. However, we can also make the stream of random numbers explicit:

def mkRNG(seed: Long): RNG = StreamRNG(stream(seed))

case class StreamRNG(stream: LazyList[Int]) extends RNG {
    def nextInt: (RNG, Int) = (StreamRNG(stream.tail), stream.head)
}

def stream(seed: Long): LazyList[Int] = {
    val random = new Random(seed)
    def mkStream: LazyList[Int] = random.nextInt #:: mkStream
    mkStream
}

The important part is the function stream with its recursive inner function mkStream. It produces a lazily evaluated, deterministic stream of random numbers with a private, mutable scala.util.Random object. Elements of this stream are memoized, therefore the expression random.nextInt is only evaluated once per stream element.

A StreamRNG object points to a specific element in the stream, and to get the next random number we just return the head of the stream and use the tail as the new state. Different StreamRNG objects can point to different elements in the stream, but the overall sequence of elements is shared between them. In addition, the head of the stream gets garbage-collected if it is no longer accessible by any StreamRNG object.

Bonus: Wrapping a purely functional random number generator in an imperative random number generator

The reverse direction is easier.

class MyRandom(seed: Int) {
    var rng: RNG = mkRNG(seed)
    def nextInt: Int = {
        val (rng2, r) = rng.nextInt
        rng = rng2
        r
    }
}

Saving the current state in a var allows overriding it, restoring mutability.