Creates a memoized function from a named or anonymous function. Calls to the memoized function are served from cache after the first execution for a given key.

Runtime controls:

  • memo.force = TRUE: ignore an existing cached value and recompute

  • memo.dryrun = TRUE: do not execute or cache; return TRUE if execution would occur, FALSE if a cache hit exists

By default, NULL results are not cached. Set allow.null = TRUE to cache NULL values as valid outcomes.

Hashing strategy for stored values:

  • id component: explicit id when provided, otherwise an inferred function name when available

  • function component: hash of the function formals and body (or function_hash_override when supplied)

  • call component: hash of normalized call arguments (including defaulted arguments, ordered by argument name)

The final storage key is a hash over these three components. This means that, by default, changing a function's implementation invalidates old cache entries for future reads while leaving historical values in storage.

Matching keys only share cached values when memo calls use the same underlying storage. Separate in-memory memo instances do not share values, even when keys are identical.

memo(f, id = NULL, function_hash_override = NULL, allow.null = FALSE)

Arguments

f

function to memoise

id

optional identifier used to scope cache keys; defaults to the function name when available

function_hash_override

optional override value used in place of the default function identity hash; can be used with explicit or inferred id

allow.null

if TRUE, the memoized function caches NULL results; FALSE by default

Value

the memoed function

Examples

library(magrittr)

# --- Basic memoization -----------------------------------------------------

# a simple function
simple.function <- function (value) {
  print("Executing!")
  value
}

# memoize and keep the new function separate
simple.function.memo <- memo(simple.function)

# id defaults to the function name when available
attr(simple.function.memo, "memo.id")
#> [1] "simple.function"

# memoize in place
simple.function %<>% memo()

# memoize anonymous functions
simple.function2 <- (function (value) value) %>% memo()

# use an explicit id for anonymous or generated functions
anonymous.memo <- memo(function (value) value, id = "example/anon")

# use an explicit id to keep cache stable across renames or wrappers
# start from the original non-memoized function to avoid wrapping a memo twice
renamed.function <- function (value) {
  print("Executing!")
  value
}
renamed.memo <- memo(renamed.function, id = "simple.function")

# first call executes
simple.function(10)
#> [1] "Executing!"
#> [1] 10

# second call with same inputs is cached
simple.function(10)
#> [1] 10

# different inputs execute once, then cache
simple.function(20)
#> [1] "Executing!"
#> [1] 20
simple.function(20)
#> [1] 20

# --- Runtime controls ------------------------------------------------------

# force recomputation even if cached
simple.function(10, memo.force = TRUE)
#> [1] "Executing!"
#> [1] 10

# dry-run does not execute or write; it reports whether execution would occur
simple.function(30, memo.dryrun = TRUE)   # TRUE (not cached yet)
#> [1] TRUE
simple.function(30)                        # executes and caches
#> [1] "Executing!"
#> [1] 30
simple.function(30, memo.dryrun = TRUE)   # FALSE (already cached)
#> [1] FALSE

# --- NULL handling ---------------------------------------------------------

# by default NULL results are not cached
null.default <- memo(function (value) {
  print("Executing NULL function")
  NULL
})
null.default(1)
#> [1] "Executing NULL function"
#> NULL
null.default(1)
#> [1] "Executing NULL function"
#> NULL

# allow.null = TRUE caches NULL values too
null.cached <- memo(function (value) {
  print("Executing NULL function with allow.null")
  NULL
}, allow.null = TRUE)
null.cached(1)
#> [1] "Executing NULL function with allow.null"
#> NULL
null.cached(1)
#> NULL

# --- Function hash override ------------------------------------------------

# use persistent storage to demonstrate cache reuse across separate memo instances
base.dir <- file.path(tempdir(), "memofunc-example-function-hash")
unlink(base.dir, recursive = TRUE, force = TRUE)

old.options <- options(memofunc.storage.provider = list(name = "file", base.dir = base.dir))

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

f.v1 <- function (value) {
  counter$executed <- counter$executed + 1
  value
}

f.v2 <- function (value) {
  counter$executed <- counter$executed + 1
  value + 1
}

# default behavior: changed body creates a new function hash component
memo(f.v1, id = "shared-id")(100)
#> [1] 100
counter$executed <- 0
memo(f.v2, id = "shared-id")(100)
#> [1] 101
counter$executed
#> [1] 1

# override: both implementations share function hash component intentionally
memo(f.v1, id = "shared-id", function_hash_override = "stable-token")(200)
#> [1] 200
counter$executed <- 0
memo(f.v2, id = "shared-id", function_hash_override = "stable-token")(200)
#> [1] 200
counter$executed
#> [1] 0

# this reuse only works because both memos share the same file-backed store above
# with separate in-memory memo instances, matching keys still would not share values

options(old.options)
unlink(base.dir, recursive = TRUE, force = TRUE)

# --- Performance example ---------------------------------------------------

slow.function <- (function (value) Sys.sleep(value)) %>% memo(allow.null = TRUE)
system.time(slow.function(2))
#>    user  system elapsed 
#>   0.002   0.000   2.004 
system.time(slow.function(2))
#>    user  system elapsed 
#>   0.001   0.000   0.001