Skip to content

Commit

Permalink
Documentation for MemoizedDeepRecursiveFunction (#215)
Browse files Browse the repository at this point in the history
Fixes #213
  • Loading branch information
serras authored Jul 12, 2023
1 parent 68ba4f2 commit 647bf6a
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 30 deletions.
103 changes: 103 additions & 0 deletions content/docs/learn/collections-functions/memoize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
sidebar_position: 3
description: Avoiding duplicate work for pure functions
---

# Memoization

Say that your function is pure, that is, given the same inputs it always
produces the same outputs and it doesn't produce any other effects like printing
to the screen. Then, once you execute the function for a given input, you could
save (or cache) the result, so the next time you need you don't to compute it
again. The general technique of saving outputs to avoid double computation of
pure functions is known as _memoization_.

## Simple memoization

<!--- TEST_NAME MemoizationTest -->

<!--- INCLUDE .*
import io.kotest.matchers.shouldBe
-->

Arrow Core contains a small utility called
[`memoize`](https://apidocs.arrow-kt.io/arrow-core/arrow.core/memoize.html)
which transforms any function into one that keep a cache of computed results.

```kotlin
import arrow.core.memoize

fun expensive(x: Int): Int {
// fake it by sleeping the thread
Thread.sleep(x * 100L)
return x
}

val memoizedExpensive = ::expensive.memoize()
```

The first time you call `memoizeExpensive`, it needs to compute the value.
From that moment on, the call returns immediately.

```kotlin
fun example() {
val result1 = memoizedExpensive(3)
val result2 = memoizedExpensive(3)
result1 shouldBe result2
}
```
<!--- KNIT example-memoize-01.kt -->
<!--- TEST assert -->

:::caution Memoization takes memory

If you define the memoized version of your function as a `val`, as we've done
above, the cache is shared among **all** calls to your function. In the worst
case, this may result in memory which cannot be reclaimed throughout the whole
execution, so you should apply this technique carefully.

There's some literature about [eviction policies for memoization](https://otee.dev/2021/08/18/cache-replacement-policy.html),
but at the moment of writing memoize doesn't offer any type of control over the
cached values. [Aedile](https://github.com/sksamuel/aedile) is a Kotlin-first
caching library which you can use to manually tweak your memoization.

:::

## Recursion

The technique outline above can be applied to any function, regardless of its
provenance. However, one needs to be aware of the limitations of `memoize` with
respect to recursive functions.

Let's say we define a recursive Fibonacci function, and call `memoize` with the
intention of avoiding computing the same values over and over.

<!--- INCLUDE
import arrow.core.memoize
-->
```kotlin
fun fibonacciWorker(n: Int): Int = when (n) {
0 -> 0
1 -> 1
else -> fibonacciWorker(n - 1) + fibonacciWorker(n - 2)
}

val fibonacci = ::fibonacciWorker.memoize()
```

<!--- INCLUDE
fun example() {
fibonacci(6) shouldBe 8
}
-->
<!--- KNIT example-memoize-02.kt -->
<!--- TEST assert -->

This solution falls short, though, because recursion goes through
`fibonacciWorker`, which is **not** memoized.

One way to avoid this problem is making `fibonacciWorker` call `fibonacci`
instead. Our recommendation, however, is to use
[`MemoizedDeepRecursiveFunction`](../recursive/#memoized-recursive-functions),
which avoids the weird mutually-recursive definition, and has the additional
benefit of avoiding stack overflows.
31 changes: 14 additions & 17 deletions content/docs/learn/collections-functions/recursive.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ can call it as if it was a function, so no changes are required for `fibonacci`.

:::

## Memoization
## Memoized recursive functions

There's an enormous amount of duplicate work being done in a call to `fibonacci`.
Here is the call tree of `fibonacciWorker(4)`, you can see that we end up in
Expand Down Expand Up @@ -125,33 +125,30 @@ Fibonacci is a pure function, in other words, given the same argument we always
obtain the same result. This means that once we've computed a value, we can just
_record_ in some cache, so later invocations only have to look there. This
technique is known as **memoization**, and Arrow provides an implementation
in the form of [`memoize`](https://apidocs.arrow-kt.io/arrow-core/arrow.core/memoize.html).
in the form of [`MemoizedDeepRecursiveFunction`](https://apidocs.arrow-kt.io/arrow-core/arrow.core/-memoized-deep-recursive-function.html).
No changes other than the outer call are required.

```kotlin
import arrow.core.memoize

val fibonacciMemoized = ::fibonacciWorker.memoize()
import arrow.core.MemoizedDeepRecursiveFunction

val fibonacciWorker = MemoizedDeepRecursiveFunction<Int, Int> { n ->
when (n) {
0 -> 0
1 -> 1
else -> callRecursive(n - 1) + callRecursive(n - 2)
}
}
```
<!--- INCLUDE
fun fibonacci(n: Int): Int {
require(n >= 0)
return fibonacciMemoized(n)
return fibonacciWorker(n)
}
fun example() {
fibonacci(6) shouldBe 8
}
```

<!--- INCLUDE
fun fibonacciWorker(n: Int): Int = when (n) {
0 -> 0
1 -> 1
else -> fibonacciWorker(n - 1) + fibonacciWorker(n - 2)
}
-->

<!--- KNIT example-recursive-03.kt -->
<!--- TEST assert -->

Expand Down
2 changes: 1 addition & 1 deletion content/docs/learn/collections-functions/utils.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 3
sidebar_position: 4
description: Composition, partial application, and currying
---

Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ coroutines = "1.7.2"
kotest = "5.6.2"
kotlin = "1.8.22"
knit = "0.4.0"
arrow = "1.2.0-RC"
arrow = "1.2.0"
ksp = "1.8.22-1.0.11"
suspendapp = "0.4.0"
kotlinKafka = "0.3.1"
Expand Down
20 changes: 20 additions & 0 deletions guide/src/test/kotlin/examples/example-memoize-01.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// This file was automatically generated from memoize.md by Knit tool. Do not edit.
package arrow.website.examples.exampleMemoize01

import io.kotest.matchers.shouldBe

import arrow.core.memoize

fun expensive(x: Int): Int {
// fake it by sleeping the thread
Thread.sleep(x * 100L)
return x
}

val memoizedExpensive = ::expensive.memoize()

fun example() {
val result1 = memoizedExpensive(3)
val result2 = memoizedExpensive(3)
result1 shouldBe result2
}
17 changes: 17 additions & 0 deletions guide/src/test/kotlin/examples/example-memoize-02.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// This file was automatically generated from memoize.md by Knit tool. Do not edit.
package arrow.website.examples.exampleMemoize02

import io.kotest.matchers.shouldBe

import arrow.core.memoize

fun fibonacciWorker(n: Int): Int = when (n) {
0 -> 0
1 -> 1
else -> fibonacciWorker(n - 1) + fibonacciWorker(n - 2)
}

val fibonacci = ::fibonacciWorker.memoize()
fun example() {
fibonacci(6) shouldBe 8
}
20 changes: 9 additions & 11 deletions guide/src/test/kotlin/examples/example-recursive-03.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,20 @@ package arrow.website.examples.exampleRecursive03

import io.kotest.matchers.shouldBe

import arrow.core.memoize

val fibonacciMemoized = ::fibonacciWorker.memoize()
import arrow.core.MemoizedDeepRecursiveFunction

val fibonacciWorker = MemoizedDeepRecursiveFunction<Int, Int> { n ->
when (n) {
0 -> 0
1 -> 1
else -> callRecursive(n - 1) + callRecursive(n - 2)
}
}
fun fibonacci(n: Int): Int {
require(n >= 0)
return fibonacciMemoized(n)
return fibonacciWorker(n)
}

fun example() {
fibonacci(6) shouldBe 8
}

fun fibonacciWorker(n: Int): Int = when (n) {
0 -> 0
1 -> 1
else -> fibonacciWorker(n - 1) + fibonacciWorker(n - 2)
}

19 changes: 19 additions & 0 deletions guide/src/test/kotlin/examples/test/MemoizationTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// This file was automatically generated from memoize.md by Knit tool. Do not edit.
package arrow.core.examples.test

import io.kotest.core.spec.style.StringSpec
import arrow.website.captureOutput
import kotlinx.knit.test.verifyOutputLines

class MemoizationTest : StringSpec({
"ExampleMemoize01" {
arrow.website.examples.exampleMemoize01.example()
}

"ExampleMemoize02" {
arrow.website.examples.exampleMemoize02.example()
}

}) {
override fun timeout(): Long = 1000
}

0 comments on commit 647bf6a

Please sign in to comment.