rslt ← {larg} (⍺⍺ ##.memo cache) rarg ⍝ Function memoization. sref ← ##.cache ivec ⍝ Ref to space with initial cache. [memo] binds an ambi-valent function left operand with a cache right operand to produce a derived function. On application to arguments, the derived function retrieves previously-calculated values from the cache, which it extends when necessary with newly-calculated ones. fast_fn ← slow_fn memo cache ⍝ bind slow_fn with cache to produce fast_fn. [cache] is a reference to a namespace containing two vectors [keys] and [vals], whose items are previously encountered arguments together with their correspond- ing results. The cache is initialised using the auxiliary function [cache], which pre-populates it with any number of expected result-argument pairs. The technique, proposed by Donald Michie in 1968 ["Memo" functions and machine learning, Nature, 218, 19-22], was published as an APL2 operator by Andréas Geyer-Schultz. This more self-contained version was suggested by Stefano Lanza- vecchia and Phil Last. As with any caching system, care must be taken to remove from the cache, items that become "stale" when the cached result no longer reflects external reality. An example might be a cached file read when the "real" file component is sub- sequently updated. Access to the cache for maintenance purposes (a kind of inspection hatch), can be achieved by retaining an explicit reference to its namespace. ∆foo ← cache'' ⍝ initialise cache for slow function foo. qfoo ← foo memo ∆foo ⍝ derive "quick" version of foo. rslt ← qfoo args ··· ⍝ using qfoo extends its cache. ∆foo.(···) ⍝ maintain cache using space-reference. Expunging the explicit reference leaves the cache intact within the derived function but effectively seals off external access to it. Access can be re- covered by stopping inside memo, tabbing to the session and assigning a new external reference. ⎕ex'∆foo' ⍝ expunge handle to foo's cache. rslt ← qfoo args ··· ⍝ qfoo continues to function normally. qfoo arg [Ctrl-Enter] ⍝ trace into derived function, [Ctrl-Tab] ⍝ ... tab to session and ∆foo←⍵⍵ ⍝ ... take new external ref. Related ancillary functions [show] and [zap], included in the examples below, may be of some use but are considered "hors d'oeuvre" and so not fixed in the workspace. Technical notes: When called, the derived function: (slow_fn memo cache) examines its cache for the supplied argument(s) and if found, returns the corresponding result. Other- wise, it returns a freshly-calculated result, updating the cache en passant. memo←{ ⍝ Function memoization. ⍺←⊢ ⍝ ambi-valent. (⊂⍺ ⍵ ⍵)∊⍵⍵.keys:⍵⍵.((keys⍳⊂⍺ ⍵ ⍵)⊃vals) ⍝ arg(s) known: fetch result. ⎕IO⊃⍵⍵.(vals keys),∘⊂←(⍺ ⍺⍺ ⍵)(⍺ ⍵ ⍵) ⍝ else: calc & extend cache. } Notice that the "look-up key" is (⍺ ⍵ ⍵). This is so that a monadic call with say, a 2-item right argument is distinct from a dyadic call with 1-item left and right argument: call key ---- --- monadic: ∇ 3 4 (3 4)(3 4) ∇ 2 2 2 dyadic: 3 ∇ 4 3 4 4 2 ∇ 2 2 2 2 Auxiliary function [cache] returns a reference to an unnamed space containing two vectors "vals" and "keys". The cache is populated by calling the function with a vector of 2 or 3 item "tuples" denoting monadic: (rslt rarg) pairs, or dyadic: (rslt larg rarg) triples. cache←{ ⍝ Ref to space containing initialised cache. vals keys←↓⍉↑{ ⍝ result values and keys. (⊃⍵)(1↓⍵,¯1↑⍵) ⍝ rslt - {larg} rarg rarg. }¨1↓(⊂0 0),⍵ ⍝ initial values of cache. ⎕NS'vals' 'keys' ⍝ unnamed ref containing cache variables. } In the penultimate line above, the phrase 1↓(⊂0 0),⍵ ensures that cache's argu- ment vector has a viable prototypical item. This means that to create an empty cache we may merely: (cache''), rather than (cache 0⍴⊂0 0). To initialise the cache with values for: ÷ 1 2 ⍝ monadic ÷ 1 0.5 1 ÷ 2 ⍝ dyadic ÷ 0.5 We would call cache thus: cache ((1 0.5)(1 2)) (0.5 1 2) │ │ │ └────── triple => rslt larg rarg (dyadic case) └───────────────────── pair => rslt rarg (monadic case) Persistent Local Variables -------------------------- The technique of binding a space reference to a function may be used in general to implement persistent local variables. That is, variables that are local to a function but whose values are retained between calls. The following function can be used to generate a stream of integers on successive calls: next←(⎕ns'')∘{ ⍝ space bound as left arg. 0=⍵:⍺.n←0 ⍝ next 0: reset number stream. (⍳⍺.n+←⍵)+⍺.n ⍝ next ⍵: return next ⍵ numbers. } next 0 ⍝ reset count. next 1 ⍝ next number. 1 next 1 ⍝ next number. 2 next 2 ⍝ next two numbers. 3 4 next 1 ⍝ next number. 5 next 0 ⍝ reset count. next 3 ⍝ next three numbers, 1 2 3 As Ray Cannon observes, the technique may even be used to _share_ persistent data among several functions with no additional workspace footprint; participat- ing functions enjoy a strictly private communication. In the following example, [brak], [join], [bkts] and [sepr] share the same space to access communal local variables: [lft], [rgt] and [sep]. ┌∇brak─┐ ┌∇bkts─┐ │ │ ┌─────────┐ │ │ │ ├─←─┼─┐ ┌─┼─←─┤ │ └──────┘ │ ├─lft─┤ │ └──────┘ │ └─rgt─┘ │ ┌∇join─┐ │ ┌─sep─┐ │ ┌∇sepr─┐ │ ├─←─┼─┘ └─┼─←─┤ │ │ │ └─────────┘ │ │ └──────┘ └──────┘ 'tmp'⎕ns'' ⍝ temporary external reference to space. brak←tmp{⍺⍺.(lft,⍵,rgt)} ⍝ function to "bracket" its argument. join←tmp{↑,∘(⍺⍺.sep∘,)/⍵} ⍝ function to join items with separators. bkts←tmp{⍺⍺.(lft rgt)←⍵} ⍝ function to set bracket characters. sepr←tmp{⍺⍺.sep←⍵} ⍝ function to set separator string. ⎕ex'tmp' ⍝ remove external reference to space. bkts'[]' ⋄ sepr'-' ⍝ set bracket and separator strings. join brak¨'tic' 'tac' 'toe' ⍝ join bracketed char vectors. [tic]-[tac]-[toe] sepr'' ⍝ change separator string. join brak¨'tic' 'tac' 'toe' ⍝ join bracketed char vectors. [tic][tac][toe] bkts'''' ⋄ sepr', ' ⍝ change bracket and separator strings. join brak¨'tic' 'tac' 'toe' ⍝ join bracketed char vectors. 'tic', 'tac', 'toe' brak join'tic' 'tac' 'toe' ⍝ bracket joined char vectors. 'tic, tac, toe' Caveat: Techniques such as these, should be used with caution, if at all. They are included here in the spirit of exploration, rather than as a suggestion that they form the basis for any operational software. While interesting in their own right, they may prove difficult to maintain as the structures will almost cert- ainly be unfamiliar to the maintenance programmer and impenetrable to workspace administration tools such as those for name cross-referencing and documentation. This operator could be coded in a more elegant way if APL provided "closures". Closures are explored further in an experimental "Function Results Edition" of Dyalog. See: http://dfns.dyalog.com/downloads/fre.pdf Examples: fread←⎕fread memo(cache ⍬) ⍝ Cached file read. fread 1 19 ⍝ Read 'n cache file component. ··· fread 1 19 ⍝ Subsequent reads satisfied from cache. afib←{⎕IO←0 ⍝ Naïve coding of ⍺-fibonacci number. ⍵∊⍳⍺:⍵ ⍝ small number: done, +/⍺ afib¨⍵-1+⍳⍺ ⍝ otherwise: sum of preceding ⍺ fib numbers. } 2 afib¨⍳20 ⍝ Fibonacci sequence. 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 time←{ ⍝ Coarse function timer. ⍺←⊢ ⍝ accept monadic operand. ai←⎕AI ⍝ start the clock. rslt←⍺ ⍺⍺ ⍵ ⍝ apply function. secs←⍕0.001×1↑2↓⎕AI-ai ⍝ seconds elapsed. ↑,/⍕¨rslt': 'secs' Secs' ⍝ timed result. } 2 afib¨time⍳20 ⍝ *slow* ⍺-fibonacci sequence. 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765: 1.262 Secs afib←afib memo(cache ⍬) ⍝ Memoize afib. 2 afib¨time⍳20 ⍝ *fast* ⍺-fibonacci sequence. 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765: 0.1 Secs ⍝ Notice that afib is coded to use its own name explicitly, rather than '∇' ⍝ for the recursion. This is so that the recursive call references the newly- ⍝ assigned derived function, whereas '∇' would have referenced the original ⍝ function and circumvented the memoization. ⍝ Perhaps surprisingly, the cache can be relied upon to supply the "base values" ⍝ of the recursion, thus simplifying the initial function coding. fib←{ ⍝ Naïve coding of regular fibonacci number. +/fib¨⍵-1 2 ⍝ sum of preceding 2 fib numbers. } fib←fib memo(cache 2/¨0 1) ⍝ Cache pre-populated with base values. fib¨time⍳20 ⍝ Fast fibonacci sequence. 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765: 0.1 Secs div←÷ memo (cache ⍬) ⍝ Ambivalent operand function 2 div 7 ⍝ Dyadic use distinct from ... 0.2857142857 div 2 7 ⍝ ... monadic use. 0.5 0.1428571429 osc←{ ⍝ Oscillate - probably returns 1. 2|⍵:osc 1+3×⍵ ⍝ odd: triple 'n add-one, osc ⍵÷2 ⍝ even: halve. } ∆osc←cache⊂1 1 ⍝ Initialise cache for osc. osc←osc memo ∆osc ⍝ derive "fast" osc function. ∆osc.⎕nl 2 ⍝ cache-space contents. keys vals show←{ ⍝ Show cache [rslts ← args] ↑⍵.(vals{⍺'←',⊂¯1↓⍵}¨keys) ⍝ (dropping extra rarg). } show ∆osc ⍝ Show initial cache. 1 ← 1 osc 2 ⍝ Apply derived function, extending cache. 1 show ∆osc ⍝ Show extended cache. 1 ← 1 1 ← 2 osc 3 ⍝ Apply function again. 1 show ∆osc ⍝ Show extended cache. 1 ← 1 1 ← 2 1 ← 4 1 ← 8 1 ← 16 1 ← 5 1 ← 10 1 ← 3 zap←{ ⍝ Remove value ⍵ from cache ⍺. ⍺.{ ⍝ within cache: mask←⍵∘≡¨↓⍉↑vals keys ⍝ mask of matches. vals keys/⍨←⊂~mask ⍝ retain non-matches. }(⊃⍵)(1↓⍵,¯1↑⍵) ⍝ target value and key. } ∆osc∘zap¨(1 16)(1 10)(1 3) ⍝ Zap selected cache entries. show ∆osc ⍝ Show compacted cache. 1 ← 1 1 ← 2 1 ← 4 1 ← 8 1 ← 5 osc 3 ⍝ Re-run function. 1 show ∆osc ⍝ Partially restored cache. 1 ← 1 1 ← 2 1 ← 4 1 ← 8 1 ← 5 1 ← 10 1 ← 3 ⎕ex'∆osc' ⍝ Seal access to osc's cache. osc¨⍳10 ⍝ Derived function still operates normally. 1 1 1 1 1 1 1 1 1 1 ⍝ And finally ... delay←⎕dl memo (cache ⍬) ⍝ Cached delay. delay time 10 ⍝ Delay 10 seconds. 10.024: 10.034 Secs delay time 10 ⍝ Side-effect-free delay :-) 10.024: 0 Secs See also: UndoRedo fibonacci osc Back to: contents Back to: Workspaces