Nested Methods
A method is a declaration. The body of a method can contain declarations and expressions. Therefore, a method declaration can contain other method declarations.
To see why this is useful, lets look at a method we wrote earlier:
def cross(count: Int): Image = {
val unit = Image.circle(20)
count match {
case 0 => unit
case n => unit.beside(unit.above(cross(n-1)).above(unit)).beside(unit)
}
}
We have declared unit
inside the method cross
.
This means the declaration of unit
is only in scope within the body of cross
.
It is good practice to limit the scope of declarations to the minimum needed, to avoid accidentally shadowing other declarations.
However, let's consider the runtime behavior of cross
and we'll see that is has some undesirable characteristics.
We'll use our substitution model to expand cross(1)
.
cross(1)
// Expands to
{
val unit = Image.circle(20)
1 match {
case 0 => unit
case n => unit.beside(unit.above(cross(n-1)).above(unit)).beside(unit)
}
}
// Expands to
{
val unit = Image.circle(20)
unit.beside(unit.above(cross(0)).above(unit)).beside(unit)
}
// Expands to
{
val unit = Image.circle(20)
unit.beside(unit.above
{
val unit = Image.circle(20)
0 match {
case 0 => unit
case n => unit.beside(unit.above(cross(n-1)).above(unit)).beside(unit)
}
}
.above(unit)).beside(unit)
}
// Expands to
{
val unit = Image.circle(20)
unit.beside(unit.above
{
val unit = Image.circle(20)
unit
}
.above(unit)).beside(unit)
}
The point of this enormous expansion is to demonstrate that we're recreating unit
every time we recurse within cross
.
We can prove this is true by printing something every time unit
is created.
def cross(count: Int): Image = {
val unit = {
println("Creating unit")
Image.circle(20)
}
count match {
case 0 => unit
case n => unit.beside(unit.above(cross(n-1)).above(unit)).beside(unit)
}
}
cross(1)
// Creating unit
// Creating unit
// res1: Image = Beside(
// l = Beside(
// l = Circle(d = 20.0),
// r = Above(
// l = Above(l = Circle(d = 20.0), r = Circle(d = 20.0)),
// r = Circle(d = 20.0)
// )
// ),
// r = Circle(d = 20.0)
// )
This doesn't matter greatly for unit
because it's very small, but we could be doing something that takes up a lot of memory or time, and it's undesirable to repeat it when we don't have to.
We could solve this by shifting unit
outside of cross
.
val unit = {
println("Creating unit")
Image.circle(20)
}
// Creating unit
// unit: Image = Circle(d = 20.0)
def cross(count: Int): Image = {
count match {
case 0 => unit
case n => unit beside (unit above cross(n-1) above unit) beside unit
}
}
cross(1)
// res3: Image = Beside(
// l = Beside(
// l = Circle(d = 20.0),
// r = Above(
// l = Above(l = Circle(d = 20.0), r = Circle(d = 20.0)),
// r = Circle(d = 20.0)
// )
// ),
// r = Circle(d = 20.0)
// )
This is undesirable because unit
now has a larger scope than needed.
A better solution it to use a nested or internal method.
def cross(count: Int): Image = {
val unit = {
println("Creating unit")
Image.circle(20)
}
def loop(count: Int): Image = {
count match {
case 0 => unit
case n => unit beside (unit above loop(n-1) above unit) beside unit
}
}
loop(count)
}
cross(1)
// Creating unit
// res5: Image = Beside(
// l = Beside(
// l = Circle(d = 20.0),
// r = Above(
// l = Above(l = Circle(d = 20.0), r = Circle(d = 20.0)),
// r = Circle(d = 20.0)
// )
// ),
// r = Circle(d = 20.0)
// )
This has the behavior we're after, creating unit
only once while minimising its scope.
The internal method loop
is using structural recursion exactly as before.
We just need to ensure that we call it in cross
.
I usually name this sort of internal method loop
or iter
(short for iterate) to indicate that they're performing a loop.
This technique is just a small variation of what we've done already, but let's do a few exercises to make sure we've got the pattern.
Exercise: Chessboard
Rewrite chessboard
using a nested method so that blackSquare
, redSquare
, and base
are only created once when chessboard
is called.
def chessboard(count: Int): Image = {
val blackSquare = Image.square(30).fillColor(Color.black)
val redSquare = Image.square(30).fillColor(Color.red)
val base =
(redSquare beside blackSquare) above (blackSquare beside redSquare)
count match {
case 0 => base
case n =>
val unit = cross(n-1)
(unit beside unit) above (unit beside unit)
}
}
Here's how I did it. It has exactly the same pattern we used with boxes
.
def chessboard(count: Int): Image = {
val blackSquare = Image.square(30) fillColor Color.black
val redSquare = Image.square(30) fillColor Color.red
val base =
(redSquare beside blackSquare) above (blackSquare beside redSquare)
def loop(count: Int): Image =
count match {
case 0 => base
case n =>
val unit = loop(n-1)
(unit beside unit) above (unit beside unit)
}
loop(count)
}
Exercise: Boxing Clever
Rewrite boxes
, shown below, so that aBox
is only in scope within boxes
and only created once when boxes
is called.
val aBox = Image.square(20).fillColor(Color.royalBlue)
def boxes(count: Int): Image =
count match {
case 0 => Image.empty
case n => aBox.beside(boxes(n-1))
}
We can do this in two stages, first moving aBox
within boxes
.
def boxes(count: Int): Image = {
val aBox = Image.square(20).fillColor(Color.royalBlue)
count match {
case 0 => Image.empty
case n => aBox.beside(boxes(n-1))
}
}
Then we can use an internal method to avoid recreating aBox
on every recursion.
def boxes(count: Int): Image = {
val aBox = Image.square(20).fillColor(Color.royalBlue)
def loop(count: Int): Image =
count match {
case 0 => Image.empty
case n => aBox.beside(loop(n-1))
}
loop(count)
}