MemoFunc - A Function Memoization Package for R

Programmable memory for R functions.

memofunc memoizes R functions with explicit, predictable cache behavior. Function calls become reusable memory: repeated computations return quickly, while cache keys remain transparent and deterministic.

Why memofunc

Installation

Install from CRAN:

install.packages("memofunc")

Install latest release:

# devtools::install_github("rwetherall/memofunc")
install.packages("memofunc", repos = c('https://rwetherall.r-universe.dev', 'https://cloud.r-project.org'))

Quick Start

library(memofunc)
library(magrittr)

slow_add <- function(a, b = 1) {
  Sys.sleep(1)
  a + b
}

memo_add <- memo(slow_add)

# first call executes
system.time(memo_add(10, 2))

# second call with same inputs hits cache
system.time(memo_add(10, 2))

Memo Options and When to Use Them

Practical Examples

1) Basic memoization

simple_fn <- function(value) {
  print("Executing")
  value
}

simple_memo <- memo(simple_fn)
simple_memo(10)  # executes
simple_memo(10)  # cached

2) In-place memoization

simple_fn %<>% memo()
simple_fn(10)
simple_fn(10)

3) Explicit id for anonymous/generated functions

anon_memo <- memo(function(value) value * 2, id = "pricing/double")
anon_memo(5)
anon_memo(5)

4) Runtime controls (memo.force, memo.dryrun)

simple_memo(30, memo.dryrun = TRUE)   # TRUE if not cached yet
simple_memo(30)                        # executes and caches
simple_memo(30, memo.dryrun = TRUE)   # FALSE because cached

simple_memo(30, memo.force = TRUE)     # forces recomputation

5) Caching NULL values

returns_null <- memo(function(x) NULL)
returns_null(1)
returns_null(1)  # executes again because NULL is not cached by default

returns_null_cached <- memo(function(x) NULL, allow.null = TRUE)
returns_null_cached(1)
returns_null_cached(1)  # cached

6) Stable keys across implementation changes (advanced)

# to share cache across memo instances, use persistent storage
base_dir <- file.path(tempdir(), "memofunc-readme-function-hash")
unlink(base_dir, recursive = TRUE, force = TRUE)

old_opts <- options(memofunc.storage.provider = list(name = "file", base.dir = base_dir))
on.exit(options(old_opts), add = TRUE)
on.exit(unlink(base_dir, recursive = TRUE, force = TRUE), add = TRUE)

counter <- new.env(parent = emptyenv())
counter$n <- 0

f_v1 <- function(value) {
  counter$n <- counter$n + 1
  value
}

f_v2 <- function(value) {
  counter$n <- counter$n + 1
  value + 1
}

# default behavior: changed implementation gets a different function component
memo(f_v1, id = "shared-id")(10)  # 10 (executes)
counter$n <- 0
memo(f_v2, id = "shared-id")(10)  # 11 (executes, no cache hit from v1)
counter$n                          # 1

# override behavior: force both implementations to share function component
memo(f_v1, id = "shared-id", function_hash_override = "stable-v1")(20)  # 20 (executes)
counter$n <- 0
memo(f_v2, id = "shared-id", function_hash_override = "stable-v1")(20)  # 20 (cache hit from v1)
counter$n                                                                    # 0 (v2 did not execute)

# note: this reuse works because both calls share the same file-backed storage.
# with separate in-memory memo instances, keys may match but values are not shared.

7) Per-function invalidation

expensive <- memo(function(value) value * 10, id = "expensive")

expensive(1)
expensive(2)

# invalidate a single cached call by function arguments
invalidate(expensive, list(value = 1))

# invalidate all cached entries for this memo only
invalidate(expensive)

Hashing Strategy (How Values Are Stored)

Each memoized call is stored against a final key that combines three hashed components:

  1. id component
    • explicit id when supplied
    • otherwise inferred function name when available
  2. function component
    • default: hash of function formals + body
    • optional override: hash of function_hash_override
  3. call component
    • normalized argument values for the specific call
    • includes defaulted parameters
    • named arguments are order-insensitive (f(a = 1, b = 2) equals f(b = 2, a = 1))

Conceptually:

final_key = H(
  id_component,
  function_component,
  call_component
)

Important: key equality and storage equality are separate concerns.

Implications:

Storage Backends

By default, memoized values are stored in memory. You can opt into file or object storage.

# direct local file storage
file_storage <- storage.init("file", base.dir = file.path(tempdir(), "memofunc"))

# provider-backed object storage using local files
object_storage <- storage.init("object", provider = "file", base.dir = file.path(tempdir(), "memofunc"))

# set default provider globally
options(memofunc.storage.provider = list(name = "file", base.dir = file.path(tempdir(), "memofunc")))
storage.init()

Azure Blob example (requires AzureStor and credentials):

if (requireNamespace("AzureStor", quietly = TRUE)) {
  account <- Sys.getenv("AZURE_STORAGE_ACCOUNT")
  container <- Sys.getenv("AZURE_STORAGE_CONTAINER")
  key <- Sys.getenv("AZURE_STORAGE_KEY")
  token <- Sys.getenv("AZURE_STORAGE_TOKEN")

  if (account != "" && container != "" && (key != "" || token != "")) {
    storage.init(
      "object",
      provider = "azure.blob",
      account = account,
      container = container,
      key = if (key == "") NULL else key,
      token = if (token == "") NULL else token,
      prefix = "memofunc"
    )
  }
}

Documentation Site Workflow

pkgdown is now CI-driven:

This means local pkgdown::build_site() is optional for contributors. Update source docs (README.md, roxygen comments, .Rd inputs, _pkgdown.yml) and let CI build/publish the site. Generated docs/ output is published by CI and should not be committed from local builds.