Skip to content

Commit 2ab9140

Browse files
committed
Add docs
1 parent dc45124 commit 2ab9140

File tree

1 file changed

+66
-0
lines changed

1 file changed

+66
-0
lines changed

docs/docs/reference/metaprogramming/inline.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,72 @@ inline def fail(p1: => Any) = {
406406
fail(indentity("foo")) // error: failed on: indentity("foo")
407407
```
408408

409+
#### `memo`
410+
411+
The `memo` method is used to avoid repeated evaluation of subcomputations.
412+
Example:
413+
```
414+
class C(x: T) {
415+
def costly(x: T): Int = ???
416+
def f(y: Int) = memo(costly(x)) * y
417+
}
418+
```
419+
Let's assume that `costly` is a pure function that is expensive to compute. If `f` was defined
420+
like this:
421+
```
422+
def f(y: Int) = costly(x) * y
423+
```
424+
the `costly(x)` subexpression would be recomputed each time `f` was called, even though
425+
its result is the same each time. With the addition of `memo(...)` the subexpression
426+
in the parentheses is computed only the first time and is cached for subsequent recalculuations.
427+
The memoized program expands to the following code:
428+
```
429+
class C(x: T) {
430+
def costly(x: T): Int = ???
431+
private[this] var memo$1: T | Null = null
432+
def f(y: Int) = {
433+
if (memo$1 == null) memo$1 = costly(x)
434+
memo$1.asInstanceOf[T]
435+
} * y
436+
}
437+
```
438+
The fine-print behind this expansion is:
439+
440+
- The caching variable is placed next to the enclosing method (`f` in this case).
441+
- Its type is the union of the type of the cached expression and `Null`.
442+
- Its inital value is `null`.
443+
- A `memo(op)` call is expanded to code that tests whether the cached variable is
444+
null, in which case it reassignes the variable with the result of evaluating `op`.
445+
The value of `memo(op)` is the value of the cached variable after this conditional assignment.
446+
447+
In simple scenarios the call to `memo` is equivalent to using `lazy val`. For instance
448+
the example program above could be simulated like this:
449+
```
450+
class C(x: T) {
451+
def costly(x: T): Int = ???
452+
@threadunsafe private[this] lazy val cached = costly(x)
453+
def f(y: Int) = cached * y
454+
}
455+
```
456+
The advantage of using `memo` over lazy vals is that it's more concise. But `memo` could also be
457+
used in scenarios where lazy vals are not suitable. For instance, let's assume
458+
that the methods in class `C` above also need a given `Context` parameter.
459+
```
460+
class C(x: T) {
461+
def costly(x: T) given Context: Int = ???
462+
def f(y: Int) given (c: Context) = memo(costly(x) given c) * y
463+
}
464+
```
465+
Now, we cannot simply pull out the computation `costly(x) given c` into a lazy val since
466+
it depends on the parameter `c` which is only available inside `f`. On the other hand,
467+
it's much harder to argue that the `memo` solution is correct. One possible scenario
468+
is that we fully intend to capture and reuse only the first computation of `costly(x)`.
469+
Another possible scenario is that we do want `memo` to be semantically invisible, used
470+
for optimization only, but that we convince ourselves that `costly(x) given c` would return
471+
the same value no matter what context `c` is passed to `f`. That's a much harder argument
472+
to make, but sometimes we can derive this from the global architecture of the system we are
473+
dealing with.
474+
409475
## Implicit Matches
410476

411477
It is foreseen that many areas of typelevel programming can be done with rewrite

0 commit comments

Comments
 (0)