-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Documentation for MemoizedDeepRecursiveFunction (#215)
Fixes #213
- Loading branch information
Showing
8 changed files
with
184 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
--- | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |