Combining Random Values

<div class="callout callout-info"> In addition to the standard imports given at the start of the chapter, in this section we're assuming the following:

import doodle.random._

</div>

So far we've seen how to represent functions generating random values using the Random type, and how to make deterministic transformations of a random value using map. In this section we'll see how we can combine two independent random values.

To motivate the problem lets try writing randomConcentricCircles, which will generate concentric circles with randomly chosen hue using the utility methods we developed in the previous section.

We start with the code to create concentric circles with deterministic colors and the utilities we developed previously.

def concentricCircles(n: Int, color: Color): Image =
  n match {
    case 0 => circle(10) fillColor color
    case n => concentricCircles(n-1, color.spin(15.degrees)) on (circle(n * 10) fillColor color) 
  }
  
def randomAngle: Random[Angle] =
  Random.double.map(x => x.turns)

def randomColor(s: Normalized, l: Normalized): Random[Color] =
  randomAngle map (hue => Color.hsl(hue, s, l))
  
def randomCircle(r: Double, color: Random[Color]): Random[Image] =
  color map (fill => Image.circle(r) fillColor fill)

Our first step might be to replace circle with randomCircle like so

val randomPastel = randomColor(0.7.normalized, 0.7.normalized)
def randomConcentricCircles(n: Int): Random[Image] =
  n match {
    case 0 => randomCircle(10, randomPastel)
    case n => randomConcentricCircles(n-1) on randomCircle(n*10, randomPastel)
  }

(Note that randomConcentricCircles returns a Random[Image].)

This does not compile, due to the line

randomConcentricCircles(n-1) on randomCircle(n*10, randomPastel)

Both randomConcentricCircles and randomCircle evaluate to Random[Image]. There is no method on on Random[Image] so this code doesn't work.

Since this is a deterministic transformation of two Random[Image] values, it seems like we need some kind of method that allows us to transform two Random[Image], not just one like we can do with map. We might call this method map2 and we could imagine writing code like

randomConcentricCircles(n-1).map2(randomCircle(n*10, randomPastel)){ 
  (circles, circle) => circles on circle
}

Presumably we'd also need map3, map4, and so on. Instead of these special cases we have a more general operator, provided by a library called Cats. If we add the following import

import cats.syntax.all._

we can now write

(randomConcentricCircles(n-1), (randomCircle(n*10, randomPastel))) mapN { 
  (circles, circle) => circles on circle
}

The complete code becomes

import cats.syntax.all._

val randomPastel = randomColor(0.7.normalized, 0.7.normalized)

def randomConcentricCircles(n: Int): Random[Image] =
  n match {
    case 0 => randomCircle(10, randomPastel)
    case n =>
      (randomConcentricCircles(n-1), randomCircle(n * 10, randomPastel)) mapN {
        (circles, circle) => circles on circle
      }
  }

Example output is shown in Figure generative:random-concentric-circles.

The output of one run of `randomConcentricCircles(10).run().draw`

So what is this new mapN method? Let's look in more depth.

Tuples and the mapN method

The mapN method is something that Cats adds to tuples. We haven't encountered tuples yet, so let's learn about them first.

A tuple is a container that lets us store a fixed number of elements of different types together. Here are some examples.

("freakout", 1)
("in", 2, 4.0)
("moonage", 42, circle(10), "daydream")

We can construct tuples using the syntax above. What about deconstructing them? For this we can use pattern matching.

("a", "b", "c") match {
  case (x, y, z) => s"$x $y $z"
}

Now the mapN method allows us to transform tuples that contain ...

TODO: complete description.

This makes more sense! The result is a List where the first element of the left-hand list has been paired with all the elements of the right-hand list, then the second element, and so on.

Now we can say more precisely what the product operator is doing to the values in the boxes: it tuples them together.

Now let's return to Random. What is the product operator doing here? Random[A] |@| Random[B] is merging together two programs or computations, one producing a value of type A at the random, and the other producing a value of type B at random. The result is a program that produces at random a value of type (A, B). Once we have a box containing a value of (A, B) we can map over it to perform a deterministic transform. This is exactly what we did in randomConcentricCircles.

Finally, we should ask if this means we can pass, say, a tuple of two elements to a function that expects two parameters. Let's answer this experimentally.

val f = (a: Int, b: Int) => a + b
val tuple = (1, 2)
f(tuple)

We cannot. We'd have to write something like

val tupleToF = (in: (Int, Int)) => {
    in match {
      case (a, b) => f(a, b)
    }
}

tupleToF(tuple)

That's quite a lengthy explanation. The good news is we don't ever run into this if we just immediately mapN over the result of using the product operator, which is the usual case.

Exercises {-}

Don't forget the following imports when you attempt these exercises.

import doodle.random._
import cats.syntax.cartesian._

Randomness and Randomness {-}

What is the difference between the output of programOne and programTwo below? Why do they differ?

def randomCircle(r: Double, color: Random[Color]): Random[Image] =
  color map (fill => Image.circle(r) fillColor fill)

def randomConcentricCircles(n: Int): Random[Image] =
  n match {
    case 0 => randomCircle(10, randomPastel)
    case n =>
      (randomConcentricCircles(n-1), randomCircle(n * 10, randomPastel)) mapN {
        (circles, circle) => circles on circle
      }
  }

val circles = randomConcentricCircles(5)
val programOne = 
  (circles, circles, circles) mapN { (c1, c2, c3) => c1 beside c2 beside c3 }
val programTwo =
  circles map { c => c beside c beside c }

<div class="solution"> programOne displays three different circles in a row, while programTwo repeats the same circle three times. The value circles represents a program that generates an image of randomly colored concentric circles. Remember map represents a deterministic transform, so the output of programTwo must be the same same circle repeated thrice as we're not introducing new random choices. In programOne we merge circle with itself three times. You might think that the output should be only one random image repeated three times, not three, but remember Random preserves substitution. We can write programOne equivalently as

val programOne = 
  (randomConcentricCircles(5), randomConcentricCircles(5), randomConcentricCircles(5)) mapN { 
    (c1, c2, c3) => c1 beside c2 beside c3 
  }

which makes it clearer that we're generating three different circles.

Colored Boxes {-}

Let's return to a problem from the beginning of the book: drawing colored boxes. This time we're going to make the gradient a little more interesting, by making each color randomly chosen.

Recall the basic structural recursion for making a row of boxes

def rowOfBoxes(n: Int): Image =
  n match {
    case 0 => rectangle(20, 20)
    case n => rectangle(20, 20) beside rowOfBoxes(n-1)
  }

Let's alter this, like with did with concentric circles, to have each box filled with a random color. Hint: you might find it useful to reuse some of the utilities we created for randomConcentricCircles. Example output is shown in Figure generative:random-color-boxes.

Boxes filled with random colors.

<div class="solution"> This code uses exactly the same pattern as randomConcentricCircles.

import cats.syntax.all._

val randomAngle: Random[Angle] =
  Random.double.map(x => x.turns)

val randomColor: Random[Color] =
  randomAngle map (hue => Color.hsl(hue, 0.7.normalized, 0.7.normalized))

def coloredRectangle(color: Color): Image =
  rectangle(20, 20) fillColor color
  
val randomColorBox: Random[Image] = randomColor.map(c => coloredRectangle(c))

def randomColorBoxes(n: Int): Random[Image] =
  n match {
    case 0 => randomColorBox
    case n =>
      val box = randomColorBox
      val boxes = randomColorBoxes(n-1)
      (box, boxes) mapN { (b, bs) => b beside bs }
  }

</div>

</div>

Structured Randomness {-}

We've gone from very structured to very random pictures. It would be nice to find a middle ground that incorporates elements of randomness and structure.

Exercises→