@@ -406,6 +406,72 @@ inline def fail(p1: => Any) = {
406
406
fail(indentity(" foo" )) // error: failed on: indentity("foo")
407
407
```
408
408
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
+
409
475
## Implicit Matches
410
476
411
477
It is foreseen that many areas of typelevel programming can be done with rewrite
0 commit comments