Functional Random Generators

In this article, we study the generator.

trait Generator[+T]:
  def generate(): T

We create an integer generator as followed:

val integers = new Generator[Int]:
  val rand = java.util.Random()
  def generate() = rand.nextInt()

Creating generators

The question is, can we create other generators based on integers? The answer is yes.

val booleans = new Generator[Boolean]:
  def generate() = intergers.generate() > 0

val pairs = new Generator[(Int, Int)]:
  def generate() = (intergers.generate(), intergers.generate())

Creating generators with For-comprehension

The next question is, can we use for-comprehension to replace new syntax?

The answer is yes. As we known, for-comprehension can be used by any class which implements map, flatMap and withFilter methods. Let's add extensions to Generator.

trait Generator[+T]:
  def generate(): T

extension [T, U](g: Generator[T])
  def map(f: T => U) = new Generator[U]:
    def generate() = f(g.generate())

  def flatMap(f: T => Generator[U]) = new Generator[U]:
    def generate() = f(g.generate()).generate()

Now, the generators can be written in a more elegent way.

val booleans: Generator[Boolean] =
  for x <- integers yield x > 0

def pairs: Generator[(Int, Int)] =
  for
    x <- integers
    y <- integers
  yield (x, y)

List generator

def lists: Generator[List[Int]] =
  for
    isEmpty <- booleans
    list <-
      if isEmpty then emptyList
      else nonEmptyList
  yield list

def emptyList = single(Nil)

def nonEmptyList: Generator[List[Int]] =
  for
    head <- integers
    tail <- lists
  yield head :: tail

By the way:

def single[T](x: T) = new Generator[T]:
  def generate() = x

Tree generator

enum Tree:
  case Node(left: Tree, right: Tree)
  case Leaf(x: Int)
def trees: Generator[Tree] =
  for
    isLeaf <- booleans
    tree <- if isLeaf then leaves else nodes
  yield tree

def leaves: Generator[Tree.Leaf] =
  for
    x <- integers
  yield Tree.Leaf(x)

def nodes: Generator[Tree.Node] =
  for
    left <- nodes
    right <- nodes
  yield Tree.Node(left, right)

Random Testing Function

def test[T](g: Generator[T], runTimes: Int = 100)
           (test: T => Boolean): Unit =
  for i <- 0 until runTimes do
    val sample = g.generate()
    assert(test(sample), s"failed for $sample")
  println(s"passed for $runTime tests")

Use it

test(lists) {
  l => l.length >= 0
}