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.
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.
<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.