mirror of
				https://github.com/Brandon-Rozek/website.git
				synced 2025-11-04 07:11:14 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			194 lines
		
	
	
		
			No EOL
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			194 lines
		
	
	
		
			No EOL
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
---
 | 
						|
date: 2022-11-12 10:45:04-05:00
 | 
						|
draft: false
 | 
						|
math: true
 | 
						|
medium_enabled: true
 | 
						|
medium_post_id: edd1ef8ee314
 | 
						|
tags:
 | 
						|
- Scala
 | 
						|
- Functional Programming
 | 
						|
title: Corecursion, Unfold and Infinite Sequences
 | 
						|
---
 | 
						|
 | 
						|
Recursion takes a large problem and breaks it down until it reaches some base cases. One popular example, is the factorial function.
 | 
						|
 | 
						|
```scala
 | 
						|
def fact(x: Int): Int =
 | 
						|
    if x == 0 then
 | 
						|
        1
 | 
						|
    else if x == 1 then
 | 
						|
        1
 | 
						|
    else
 | 
						|
        x * fact(x - 1)
 | 
						|
```
 | 
						|
 | 
						|
Though we can similarly arrive at the answer by starting at the base case `1` and multiplying until we reach `x`. This is called co-recursion.
 | 
						|
 | 
						|
```
 | 
						|
1 * 2 * ... * x
 | 
						|
```
 | 
						|
 | 
						|
`Unfold` allows us to create sequences given some initial state and a function that takes some state and produces a value for the sequence. For the factorial function, we want to keep track of in our state the last factorial computed and the current index. `(lastFact, currInd)`. 
 | 
						|
 | 
						|
Therefore, our initial state is `(1, 0)`.
 | 
						|
 | 
						|
```scala
 | 
						|
val fact_seq = () => Iterator.unfold((1, 0))((x, y) => Some(
 | 
						|
    x, // currFact
 | 
						|
    (x * (y + 1), y + 1) // (nextFact, nextInd)
 | 
						|
))
 | 
						|
 | 
						|
val fact = (x: Int) => fact_seq().take(x + 1).toList.last
 | 
						|
```
 | 
						|
 | 
						|
 Let's trace an execution of `fact(4)`.
 | 
						|
 | 
						|
```
 | 
						|
fact(4)
 | 
						|
Iterator.unfold((1, 0))((x, y) => Some((x, (x * (y + 1), y + 1)))).take(5).toList.last
 | 
						|
States: (1, 0) -> (1, 1) -> (2, 2) -> (6, 3) -> (24, 4).....
 | 
						|
[1, 1, 2, 6, 24, ...].take(4).toList.last
 | 
						|
[1, 1, 2, 6, 24].last
 | 
						|
24
 | 
						|
```
 | 
						|
 | 
						|
Now why is this useful when maybe the recursive version can seem cleaner? Co-recursion and in turn unfolding can help remove redundancies. Let's look at the Fibbonaci sequence for an example. The recursive version would be as follows:
 | 
						|
 | 
						|
```scala
 | 
						|
def fib(n : Int): Int =
 | 
						|
    if n == 0 then
 | 
						|
        0
 | 
						|
    else if n == 1 then
 | 
						|
        1
 | 
						|
    else
 | 
						|
        fib(n - 1) + fib(n - 2)
 | 
						|
```
 | 
						|
 | 
						|
Now let's trace through an execution of `fib(4)`
 | 
						|
 | 
						|
```
 | 
						|
fib(4)
 | 
						|
fib(3) + fib(2)
 | 
						|
(fib(2) + fib(1)) + fib(2)
 | 
						|
((fib(1) + fib(0)) + fib(1)) + fib(2)
 | 
						|
((1 + fib(0)) + fib(1)) + fib(2)
 | 
						|
((1 + 0) + fib(1)) + fib(2)
 | 
						|
(1 + fib(1)) + fib(2)
 | 
						|
(1 + 1) + fib(2)
 | 
						|
2 + fib(2)
 | 
						|
2 + (fib(1) + fib(0))
 | 
						|
2  + (1 + fib(0))
 | 
						|
2 + (1 + 0)
 | 
						|
2 + 1
 | 
						|
3
 | 
						|
```
 | 
						|
 | 
						|
Notice how there are many redundant calculations, for example `fib(2)` is evaluated twice separately in line 3 above.
 | 
						|
 | 
						|
Now lets look at how `unfold` helps.  For our state, we need to keep track of the last two evaluations. Therefore, we can represent our state as `(currentAnswer, nextAnswer)`.
 | 
						|
 | 
						|
```scala
 | 
						|
val fib_seq = () => Iterator.unfold((0, 1))((x, y) => Some(x, (y, x + y)))
 | 
						|
val fib: (Int => Int) = (n) => fib_seq.take(n + 1).toList.last
 | 
						|
```
 | 
						|
 | 
						|
Tracing through `fib(4)`
 | 
						|
 | 
						|
```
 | 
						|
fib(4)
 | 
						|
Iterator.unfold((0, 1))((x, y) => Some(x,(y, x + y))).take(5).toList.last
 | 
						|
State: (0, 1) -> (1, 1) -> (1, 2) -> (2, 3) -> (3, 5)
 | 
						|
[0, 1, 1, 2, 3, ...].take(5).toList.last
 | 
						|
[0, 1, 1, 2, 3].last
 | 
						|
3
 | 
						|
```
 | 
						|
 | 
						|
## Small Unfold Examples
 | 
						|
 | 
						|
To get a better handle of `unfold`. Here are three examples:
 | 
						|
 | 
						|
**(1) Build an iterator from start to infinity with a step size of `step`**
 | 
						|
 | 
						|
Built-in way in Scala:
 | 
						|
 | 
						|
```scala
 | 
						|
Iterator.from(start, step)
 | 
						|
```
 | 
						|
 | 
						|
Using `unfold`
 | 
						|
 | 
						|
```scala
 | 
						|
val fromStep: ((Int, Int) => Iterator[Int]) = (n, step) => Iterator.unfold(n)(x => Some((x, x + step)))
 | 
						|
```
 | 
						|
 | 
						|
**(2) Build an infinite sequence of even numbers**
 | 
						|
 | 
						|
Using from and map:
 | 
						|
 | 
						|
```scala
 | 
						|
val evens = Iterator.from(0).filter(_ % 2 == 0)
 | 
						|
```
 | 
						|
 | 
						|
Using `fromStep` in (1)
 | 
						|
 | 
						|
```scala
 | 
						|
val evens = fromStep(0, 2)
 | 
						|
```
 | 
						|
 | 
						|
Using `unfold`
 | 
						|
 | 
						|
```scala
 | 
						|
val evens = Iterator.unfold(0)(x => Some((x, x + 2)))
 | 
						|
```
 | 
						|
 | 
						|
**(3)  Build a countdown from $n$ to $0$**
 | 
						|
 | 
						|
Notice how the function within `unfold` needs to return an `Option`. If the returned option is `None` then the sequence is terminated.
 | 
						|
 | 
						|
```scala
 | 
						|
val countdown = (n: Int) => Iterator.unfold(n)(x => if x == -1 then None else Some((x, x - 1)))
 | 
						|
```
 | 
						|
 | 
						|
**(4) Repeat an element forever**
 | 
						|
 | 
						|
For this example, we don't need to carry any state throughout hte computation.
 | 
						|
```scala
 | 
						|
val repeat: (Int => Iterator[Int]) = (n) => Iterator.unfold(None)(_ => Some(n, None))
 | 
						|
```
 | 
						|
 | 
						|
## Recursive Sequences
 | 
						|
 | 
						|
In the past, [I've written](/blog/haskellrealsequences/) about analyzing sequences from real analysis within Haskell. Within it, I was looking at the following sequence:
 | 
						|
$$
 | 
						|
f(1) = 1, f(2) = 2, f(n) = \frac{1}{2}(f(n - 2) + f(n - 1))
 | 
						|
$$
 | 
						|
The technique I described in that post is to build out the function `f` and then map it to the natural numbers. In Scala that would look like:
 | 
						|
 | 
						|
```scala
 | 
						|
val f: (Int => Double) = n => if n == 1 then 1.0 else if n == 2 then 2.0 else 0.5 * (f(n - 2) + f(n - 1))
 | 
						|
val f_sequence = Iterator.from(1).map(f)
 | 
						|
```
 | 
						|
 | 
						|
However as mentioned in prior in this post, this methodology is suboptimal since there will be many repeated computations. 
 | 
						|
 | 
						|
Corecursion and unfold comes to the rescue again. For recursive sequences, we can make the state the base cases `(1.0, 2.0)`.
 | 
						|
 | 
						|
```scala
 | 
						|
val f_sequence = () => Iterator.unfold((1.0, 2.0))((x1, x2) => Some(
 | 
						|
    x1,
 | 
						|
    (x2, 0.5 * (x1 + x2))
 | 
						|
))
 | 
						|
```
 | 
						|
 | 
						|
We can get a good guess at where this sequence converges by looking at the $100^{th}$ element.
 | 
						|
 | 
						|
```scala
 | 
						|
f_sequence().take(100).toList.last
 | 
						|
// 1.6666666666666665
 | 
						|
```
 | 
						|
 | 
						|
If you want to learn more about unfold or see a different take, then the following two blog posts helped me craft this one:
 | 
						|
 | 
						|
https://blog.genuine.com/2020/07/scala-unfold/
 | 
						|
 | 
						|
https://john.cs.olemiss.edu/~hcc/csci555/notes/FPS05/Laziness.html |