Now that we understand what generative recursion is, we’re going to generalise an existing generative recursion function so that it can be used to express all others.
charCodes
We’re going to start from charCodes
, whose code is:
def charCodes(
from: String
): List = {
if(from.nonEmpty)
Cons(from.head.toInt, charCodes(from.tail))
else Nil
}
As before, our first step is going to be to rename it. Let’s call it recurse
because why not.
def recurse(
from: String
): List = {
if(from.nonEmpty)
Cons(from.head.toInt, recurse(from.tail))
else Nil
}
Here’s a diagram that represents how charCodes
behaves:
We first check whether the input string is empty. If it’s not, we extract its head (as an int) and tail, then recurse on the later to get the tail of our new list.
Finally, we create a new list from that optional head and tail.
Our first step is going to be to generalise that nonEmpty
predicate:
As we did before for fold
, this is relatively straightforward. We’ll start by creating a predicate
function that does the same thing:
def predicate(from: String): Boolean =
from.nonEmpty
After which we can update recurse
to take predicate
as a parameter, and use that instead of the hard-coded nonEmpty
.
def recurse(
predicate: String => Boolean,
from : String
): List = {
if(predicate(from))
Cons(from.head.toInt, recurse(predicate, from.tail))
else Nil
}
This is already a significant improvement: we have removed some hard-coded magic.
The next step is to take care of the hard-coded bit that turns a string into its head and tail.
We can do so by extracting the logic into a function that takes a String
and splits it into a tuple:
def update(from: String): (Int, String) =
(from.head.toInt, from.tail)
This forces us to rewrite recurse
a little bit to take update
as a parameter, but the logic is still the same:
def recurse(
predicate: String => Boolean,
update : String => (Int, String),
from : String
): List = {
if(predicate(from)) {
val (head, nextState) = update(from)
Cons(head, recurse(predicate, update, nextState))
}
else Nil
}
And we’ve now gotten rid of all the hard-coded value-level bits:
We’re still limited to taking strings as input, though.
Luckily, this can easily be worked around: we never actually use the fact that our state is a string. Our only requirement is that it’s the same type that’s manipulated by predicate
and update
, which we can express through a type parameter:
def recurse[A](
predicate: A => Boolean,
update : A => (Int, A),
from : A
): List = {
if(predicate(from)) {
val (head, nextState) = update(from)
Cons(head, recurse(predicate, update, nextState))
}
else Nil
}
We now have a new recurse
function that abstracts over its input type, predicate and update functions.
We’re almost done, but for the same readability reasons as with fold
, I want to turn the body of recurse
into an internal helper function:
def recurse[A](
predicate: A => Boolean,
update : A => (Int, A),
from : A
): List = {
def loop(state: A): List =
if(predicate(state)) {
val (head, nextState) = update(state)
Cons(head, loop(nextState))
}
else Nil
loop(from)
}
This yields the final diagram for our recurse
function:
The observation we made while writing fold
that it was just passing its parameters directly to loop
still holds, so we’ll do the same thing we did then: have recurse
return loop
directly.
def recurse[A](
predicate: A => Boolean,
update : A => (Int, A)
): A => List = {
def loop(state: A): List =
if(predicate(state)) {
val (head, nextState) = update(state)
Cons(head, loop(nextState))
}
else Nil
loop
}
And this is a pretty decent implementation of generative recursion, stack safety issues aside.
Before we can move on, we must of course do the functional programmer thing and give it a proper name.
This is commonly known as unfold
, which I find sort of poetic: where we used fold
to sort of collapse a list onto itself and turn it into a single value, unfold
does the opposite by unfolding a single value into a list.
def unfold[A](
predicate: A => Boolean,
update : A => (Int, A)
): A => List = {
def loop(state: A): List =
if(predicate(state)) {
val (head, nextState) = update(state)
Cons(head, loop(nextState))
}
else Nil
loop
}
And our unfold
still does exactly the same thing charCodes
did:
mkString(unfold(predicate, update)("cata"))
// res31: String = 99 :: 97 :: 116 :: 97 :: nil
range
as an unfoldWe know have an unfold
implementation that should allow us to write recursive generation functions more comfortably. We know charCodes
works - this is what we’ve been doing all along, but what about range
?
val range: Int => List =
unfold[Int](
predicate = state => state > 0,
update = state => (state, state - 1)
)
We apply exactly the same logic as in our first implementation, but without having to do the boilerplate work:
head
, and decrement the old state by 1 to get the new one.This, of course, behaves exactly as before:
mkString(range(3))
// res32: String = 3 :: 2 :: 1 :: nil
We’ve seen that generative recursion was straightforward to generalise: turn its predicate and update function into parameters.
This allows us to have a general generative recursion function, unfold
. General, that is, provided you’re working with List
as output…