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 make a random (or stochastic, if you prefer fancier words) transformation of a random value using flatMap
.
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(count: Int, size: Int, color: Color): Image =
count match {
case 0 => Image.empty
case n =>
Image.circle(size).fillColor(color).on(concentricCircles(n-1, size + 5, color.spin(15.degrees)))
}
val randomAngle: Random[Angle] =
Random.double.map(x => x.turns)
def randomColor(s: Double, l: Double): 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))
Let's create a method skeleton for randomConcentricCircles
.
def randomConcentricCircles(count: Int, size: Int): Random[Image] =
???
The important change here is we return a Random[Image]
not an Image
.
We know this is a structural recursion over the natural numbers so we can fill out the body a bit.
def randomConcentricCircles(count: Int, size: Int): Random[Image] =
count match {
case 0 => ???
case n => ???
}
The base case will be Random.always(Image.empty)
, the direct of equivalent of Image.empty
in the deterministic case.
def randomConcentricCircles(count: Int, size: Int): Random[Image] =
count match {
case 0 => Random.always(Image.empty)
case n => ???
}
What about the recursive case? We could try using
val randomPastel = randomColor(0.7, 0.7)
def randomConcentricCircles(count: Int, size: Int): Random[Image] =
count match {
case 0 => Image.empty
case n =>
randomCircle(size, randomPastel).on(randomConcentricCircles(n-1, size + 5))
}
// error:
// Found: (doodle.image.Image.empty : doodle.image.Image)
// Required: doodle.random.Random[doodle.image.Image]
// error:
// value on is not a member of doodle.random.Random[doodle.image.Image]
// randomCircle(size, randomPastel).on(randomConcentricCircles(n-1, size + 5))
// ^
// error:
// Line is indented too far to the left, or a `}` is missing
// error:
// Line is indented too far to the left, or a `}` is missing
// error:
// Line is indented too far to the left, or a `}` is missing
// error:
// Line is indented too far to the left, or a `}` is missing
// error:
// Line is indented too far to the left, or a `}` is missing
// error:
// Line is indented too far to the left, or a `}` is missing
but this does not compile.
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 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
randomCircle(size, randomPastel).map2(randomConcentricCircles(n-1, size + 5)){
(circle, circles) => circle on circles
}
Presumably we'd also need map3
, map4
, and so on.
Instead of these special cases we can use flatMap
and map
together.
randomCircle(size, randomPastel) flatMap { circle =>
randomConcentricCircles(n-1, size + 5) map { circles =>
circle on circles
}
}
The complete code becomes
def randomConcentricCircles(count: Int, size: Int): Random[Image] =
count match {
case 0 => Random.always(Image.empty)
case n =>
randomCircle(size, randomPastel).flatMap{ circle =>
randomConcentricCircles(n-1, size + 5).map{ circles =>
circle.on(circles)
}
}
}
Example output is shown in Figure generative:random-concentric-circles.
Let's now look closer at this use of flatMap
and map
to understand how this works.
Type Algebra
The simplest way, in my opinion, to understand why this code works is to look at the types. The code in question is
randomCircle(size, randomPastel) flatMap { circle =>
randomConcentricCircles(n-1, size + 5) map { circles =>
circle on circles
}
}
Starting from the inside, we have
{ circles =>
circle on circles
}
which is a function with type
Image => Image
Wrapping this we have
randomConcentricCircles(n-1, size + 5) map { circles =>
circle on circles
}
We known randomConcentricCircles(n-1, size + 5)
has type Random[Image]
.
Substituting in the Image => Image
type we worked out above we get
Random[Image] map (Image => Image)
Now we can deal with the entire expression
randomCircle(size, randomPastel) flatMap { circle =>
randomConcentricCircles(n-1, size + 5) map { circles =>
circle on circles
}
}
randomCircle(size, randomPastel)
has type Random[Image]
.
Performing substitution again gets us a type equation for the entire expression.
Random[Inage] flatMap (Random[Image] map (Image => Image))
Now we can apply the type equations for map
and flatMap
that we saw earlier:
F[A] map (A => B) = F[B]
F[A] flatMap (A => F[B]) = F[B]
Working again from the inside out, we first use the type equation for map
which simplifies the type expression to
Random[Inage] flatMap (Random[Image])
Now we can apply the equation for flatMap
yielding just
Random[Image]
This tells us the result has the type we want. Notice that we've been performing substitution at the type level---the same technique we usually use at the value level.
Exercises {-}
Don't forget to import doodle.random._
when you attempt these exercises.
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(count: Int, size: Int): Random[Image] =
count match {
case 0 => Random.always(Image.empty)
case n =>
randomCircle(size, randomPastel).flatMap{ circle =>
randomConcentricCircles(n-1, size + 5).map{ circles =>
circle.on(circles)
}
}
}
val circles = randomConcentricCircles(5, 10)
val programOne =
circles.flatMap{ c1 =>
circles.flatMap{ c2 =>
circles.map{ 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 programOneRewritten =
randomConcentricCircles(5, 10) flatMap { c1 =>
randomConcentricCircles(5, 10) flatMap { c2 =>
randomConcentricCircles(5, 10) map { c3 =>
c1 beside c2 beside c3
}
}
}
// programOneRewritten: Free[[A >: Nothing <: Any] => RandomOp[A], Image] = FlatMapped(
// c = FlatMapped(
// c = FlatMapped(
// c = FlatMapped(
// c = FlatMapped(
// c = Suspend(a = RDouble),
// f = cats.free.Free$$Lambda$14440/0x000000010396b840@3f8e6b0c
// ),
// f = cats.free.Free$$Lambda$14440/0x000000010396b840@10ddc9cb
// ),
// f = cats.free.Free$$Lambda$14440/0x000000010396b840@13735c19
// ),
// f = repl.MdocSession$MdocApp5$$Lambda$14457/0x000000010397d040@5ea1278c
// ),
// f = repl.MdocSession$MdocApp5$$Lambda$14460/0x000000010397b040@123bf15f
// )
which makes it clearer that we're generating three different circles. </div>
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(count: Int): Image =
count match {
case 0 => Image.rectangle(20, 20)
case n => Image.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
.
val randomAngle: Random[Angle] =
Random.double.map(x => x.turns)
// randomAngle: Free[[A >: Nothing <: Any] => RandomOp[A], Angle] = FlatMapped(
// c = Suspend(a = RDouble),
// f = cats.free.Free$$Lambda$14440/0x000000010396b840@6cd6f142
// )
val randomColor: Random[Color] =
randomAngle.map(hue => Color.hsl(hue, 0.7, 0.7))
// randomColor: Free[[A >: Nothing <: Any] => RandomOp[A], Color] = FlatMapped(
// c = FlatMapped(
// c = Suspend(a = RDouble),
// f = cats.free.Free$$Lambda$14440/0x000000010396b840@6cd6f142
// ),
// f = cats.free.Free$$Lambda$14440/0x000000010396b840@654cfb55
// )
def coloredRectangle(color: Color): Image =
Image.rectangle(20, 20).fillColor(color)
def randomColorBoxes(count: Int): Random[Image] =
count match {
case 0 => randomColor.map{ c => coloredRectangle(c) }
case n =>
val box = randomColor.map{ c => coloredRectangle(c) }
val boxes = randomColorBoxes(n-1)
box.flatMap{ b =>
boxes.map{ bs => b.beside(bs) }
}
}
</div>