- pure functions that are expensive to compute, call repeatedly, and produce
relatively small results should be memoized; ie check if the arguments were
seen before, and if they were, fetch the result, and if not, compute the result
and cache it for future lookups
- eg query steps against a read-only db
- eg algorithms whose implementation lends naturally to top-down dynamic programming
- the process of memoizing a function should be agnostic to the function,
undoable, configurable, and easy
- lru semantics for cache eviction
name | description |
.memo.C | cache capacity. defaults to 100h . can be any short |
.memo.P | cache table parent structure name. defaults to `cache . can be any symbol |
name | description |
.memo.init | initializes a cache table given a namespace and capacity |
.memo.initns | initializes a cache table given a namespace |
.memo.initcap | initializes a cache table given a capacity |
.memo.mk | makes a memoized copy of a function given a source, target, and cache name |
.memo.mkmut | mutates a given function into its memoized copy given a source |
.memo.mknew | makes a memoized copy of a function given a source and target |
.memo.rm | removes a memoized function and clears its cached data given a source |
.memo.mv | moves a memoized function to a new cache table given a source and cache name |
.memo.cap | returns the capacity of a cache table given a cache name |
.memo.rs | resizes a cache table given a cache name and capacity |
q)/ .memo.init takes a [namespace;<short>]
q).memo.init[`.;2h]
`..cache.0
q)/ a cache table, named `0, is created within `cache in `.
q)cache
| ::
0| (+`f`a!(,`;,::))!+(,`r)!,,::
q)/ .memo.initns is shorthand for .memo.init, using .memo.C as the cache capacity
q).memo.initns[]
`..cache.1
q)/ .memo.initcap is shorthand for .memo.init, using \d as the namespace
q)\d .a
q).memo.initcap 2h
`.a.cache.0
q)/ (::) or ` may be used to reference \d
q).memo.initns`
`.a.cache.1
q)/ .memo.mk takes a [<function>;<symbol>;<symbol>]
q)f:sum;.memo.mk[`f;`g;`cache.0]
`.a.g
q)/ `g is a memoized copy of `f with the same arity, and it points to `.a.cache.0
q)g 1 2 3
6
q)g 4 5 6
15
q)/ since 1 2 3 and 4 5 6 are new args, `g computes them and caches the results
q)/ in `.a.cache.0
q)cache.0
f a | r
-----------| --
:: | ::
.a.g ,1 2 3| 6
.a.g ,4 5 6| 15
q)/ subsequent invocations with recognized arguments will fetch from
q)/ here--instead of executing the underlying implementation
q)g 4 5 6
q)cache.0
f a | r
-----------| --
:: | ::
.a.g ,1 2 3| 6
.a.g ,4 5 6| 15
q)/ .memo.mkmut is shorthand for .memo.mk, mutating the function in place and
q)/ using the most recent cache table
q)h:div;.memo.mkmut`h
`.a.h
q)/ .memo.mknew is shorthand for .memo.mk, using the most recent cache table
q)j:prd;.memo.mknew[`j;`j1]
`.a.j1
q)/ .memo.mk can also take a function as a lambda
q).memo.mk[min;`f1;`cache.1]
`.a.f1
q)/ a null second argument will use the same name as the first, like .memo.mkmut
q)f2:max;.memo.mk[`f2;`;`cache.0]
`.a.f2
q)/ removing a memoized function that was made in place reverts the
q)/ implementation
q).memo.rm`h
`.a.h
q)h
div
q)/ if made with a literal, the source literal is returned
q).memo.rm`f1
min
q)/ if made with a symbol, the source symbol is returned
q).memo.rm`g
`.a.f
q)/ in all cases, the memoized copy no longer exists
q)g
'g
q)/ any removal of a memoized function clears its cached data
q)cache.0
f a | r
----| --
::| ::
q)/ .memo.mv takes a [<symbol>;<symbol>] as its function and cache table,
q)/ respectively
q)j1 2 3 4
q).memo.mv[`j1;`..cache.0]
`.a.j1
q)/ its cached data has moved
q)cache.1
f a | r
----| --
::| ::
q)\d .
q)cache.0
f a | r
------------| --
:: | ::
.a.j1 ,2 3 4| 24
q)/ and it now points to this new cache, too
q).a.j1 5 6 7
210
q)cache.0
f a | r
------------| ---
:: | ::
.a.j1 ,2 3 4| 24
.a.j1 ,5 6 7| 210
q)/ .memo.cap takes a [cache]
q).memo.cap`cache.0
2h
q)/ in practice, you probably wouldn't want to make a cache table
q)/ this small
q)/ .memo.rs takes a [cache;<short>]
q).memo.rs[`cache.0;10h]
`.a.cache.1
q).a.j1 1 2 3;.a.j1 2 3 4;cache.0
f a | r
------------| ---
:: | ::
.a.j1 ,2 3 4| 24
.a.j1 ,5 6 7| 210
.a.j1 ,1 2 3| 6
q)/ resizing a cache to below its capacity trims it
q).memo.rs[`cache.0;1h]
`..cache.0
q)cache.0
f a | r
------------| --
:: | ::
.a.j1 ,1 2 3| 6
q)/ notice the lru semantics, here
q)/ the principal reason why .memo operates via indirection is to allow named
q)/ functions within lexical scope to magically use dynamic programming.
q)/ a stupid implementation for computing the nth fibonacci number follows
q)dumb:{$[1=x;0;x<3;1;dumb[x-1]+dumb x-2]}
q)dumb 10
34
q)\t dumb 30
497
q)/ we want to first expand the cache capacity beyond its paltry 1h
q).memo.rs[`cache.1;100h];.memo.mkmut`dumb
`..dumb
q)\t dumb 30
0
q).memo.rm[`dumb][27]~dumb 27
1b
q)/ the stupid implementation is magically hundreds of times faster
q)/ if passed as a literal, its new name can't be null
q).memo.mknew[sums;`]
'null name
q)/ don't play with the parent structure, eg
q)/ cache:10
q)/ the library will cease to function properly
q)/ don't memoize stateful functions
q).memo.mknew[rand;`why]
`..why
q)count distinct why each 10#10
1
q)/ randomness is lost
q)/ be wary of .z.s
q)megadumb:{$[1=x;0;x<3;1;.z.s[x-1]+.z.s x-2]}
q)\t megadumb 30
436
q)\t .memo.mkmut[`megadumb]30
664
q)/ recall that .z.s produces a function literal as defined at parse-time,
q)/ so cache lookups are not used anywhere except the top of the stack
q)\t megadumb 30
0
q)\t megadumb 31
766
q)/ recursively memoize by using names
q)dumb:{$[1=x;0;x<3;1;dumb[x-1]+dumb x-2]}
q)\t .memo.mkmut[`dumb]31
0
q)megadumb[32]~dumb 32
1b
q)/ as the memo tables are global data, memoized functions that need to
q)/ write to the cache, ie take new values, cannot run in parallel.
q)/ if the function can be made logically concurrent, measure to be
q)/ sure if this trade is worth it. if, however, you know that invocations
q)/ will only read from the cache, feel free to do this in parallel
q)0<system"s"
1b
q)dumb peach 10 30
34 514229
q)dumb peach 40 50
'noupdate
q)/ don't make the cache too small. the additional overhead incurred by
q)/ both cache misses and constant evictions will drastically impact
q)/ performance. this is particularly relevant in memoizing recursive
q)/ calls with a large number of leaves
q).memo.rs[.memo.initns[];1h]
`..cache.2
q).memo.mknew[.memo.rm[`dumb];`ultradumb]
`..ultradumb
q)\t ultradumb 30
550
q)/ don't memoize a function multiple times. there is absolutely no
q)/ value in doing so, and its cache behavior will get complicated
q).memo.mkmut .memo.mknew[sum;`foo]
`..foo
q)count string foo
142