kikito / memoize.lua

memoized functions in lua
https://github.com/kikito/memoize.lua
MIT License
100 stars 10 forks source link

memoize.lua

Build Status

This is a pure-Lua memoization function that builds upon what was shown in Programming In Lua's memoization implementation function.

Main characteristics:

Partially inspired by this StackOverflow question

Synopsis

local memoize = require 'memoize'

local memoized_f = memoize(f, <cache>)

Params:

Examples of use

memoize.lua can be used to avoid stack overflow & slow performance on recursive functions, in exchange for memory. In some cases it might be necessary to "seed the function" before using it.

local memoize = require 'memoize'

local function tri(x)
  if x == 0 then return 0 end
  return x+tri(x-1)
end

print(tri(40000)) -- stack overflow: too much recursion

local memoized_tri = memoize(tri) -- memoized_tri "remembers" previous results

for i=0, 40000 do memoized_tri(i) end -- seed cache

print(memoized_tri(40000)) -- 800020000, instantaneous result

Another use for memoize.lua is on resource-loading functions. Let's say you have an image-loading function called load_image. You'd like to use the image loaded by that function on two different places in your code, but you are unsure of which of those places will call load_image first; and you don't want to load two images.

function load_image(path)
  ...
  return image
end

function f()
  local image = load_image(path)
  ...
end

function g()
  local image = load_image(path)
  ...
end

You can just memoize load_image; the image will be loaded the first time load_image is invoked, and will be recovered from the cache on subsequent calls.

local memoize = require 'memoize'

local function load_image(path)
  ...
  return image
end

local memoized_load_image = memoize(load_image)

function f()
  local image = memoized_load_image(path)
  ...
end

function g()
  local image = memoized_load_image(path)
  ...
end

Gotcha / Warning

nil return values are considered cache-misses and thus are never cached; If you need to cache a function that doesn't return anything, make it return a dummy not-nil value (false, '', 0 or any other non-nil value will work just fine).

Cache utilization & structure

Each memoized function has an internal cache. If you want to recuperate the memory consumed by a memoized function's cache, the simplest way of doing so is removing all references to the memoized function. Once the collector runs, you should get the memory back.

local mf = memoize(f)
mf(1)
mf(2)
mf(3)
...
mf = nil -- memory for mf will be liberated
...
local mf2 = memoize(f) -- you can memoize the same function again if you need

memoize.lua stores the information in a tree-like structure, where the nodes are parameters and the leafs are results. For example, take the sum function, which operates on a variable list of arguments:

local sum = function(...)
  local result = 0
  local params = { ... }
  for i = 1, #params do
    result = result + params[i]
  end
  return result
end

If we memoize it and seed the memoized function like so:

local memoized_sum = memoize(sum)

print(memoized_sum())     -- 0
print(memoized_sum(1))    -- 1
print(memoized_sum(1, 2)) -- 3
print(memoized_sum(3, 1)) -- 4

The internal cache would have the following structure:

{
  results  = { 0 },
  children = {
    [1] = {
      results = { 1 },
      children = {
        [2] = {
          results = { 3 },
        }
      }
    }
    [3] = {
      children = {
        [1] = {
          result = { 4 },
        }
      }
    }
  }
}

If you use the default cache, removing all the references to the memoized function is the only way to liberate memory. If you need more control on the cache, you can use memoize's second parameter to provide a cache which you control:

local my_cache = {}

local mf = memoize(f, my_cache)

mf(1)
mf(1, 2)
mf(4)

my_cache.children[1] = nil -- partially expunge the cache

In the example above, the memory dedicated to results mf(1) and mf(1,2) has been erased, but the memory storing mf(4) is still in use.

The second parameter also allows sharing a single cache amongst two memoized functions.

Changelog

See CHANGELOG.md for details

License

MIT