Popular Python decorators ported to R with the adorn framework.
| Decorator | What it does | Python equivalent |
|---|---|---|
retry() |
Calls a function again when it fails | tenacity |
lru_cache() |
Caches results, evicting the least recently used | functools.lru_cache |
cache |
Caches results, never evicting | functools.cache |
disk_cache() |
Caches results as files, so they outlive the session | joblib.Memory.cache |
validate_args(), validate_return()
|
Checks arguments and return values with any validator function | pydantic validate_call |
circuit_breaker() |
Stops calling a function after repeated failures, then retries after a cooldown | pybreaker |
rate_limit() |
Limits how often a function can be called in a time window | ratelimit |
contextmanager |
Writes setup and teardown once, as local_, with_ and decorator forms |
contextlib.contextmanager |
# install.packages("pak")
# pak::pak("DyfanJones/garnish")
install.packages("garnish", repos = c('https://dyfanjones.r-universe.dev', 'https://cloud.r-project.org'))
retry() and lru_cache() build a decorator, which you apply to a function
with %@% or by calling it on the function.
library(garnish)
n <- 0
flaky <- function() {
n <<- n + 1
if (n < 3) stop("not yet")
n
}
retry(stop = stop_after_attempt(5))(flaky)()
#> [1] 3
Choose when to give up, how long to wait, and what to retry:
fetch := retry(
stop = stop_after_attempt(4),
wait = wait_exp(multiplier = 0.1) + wait_rand(0, 0.1),
retry = retry_if_error_message(match = "timeout"),
before_sleep = before_sleep_log()
) %@% function(url) {
# ...
}
When stop fires, a retry_error condition is signalled. Set reraise = TRUE
to re-throw the last error instead, or give retry_error_callback a function
to return a fallback value:
poll := retry(
stop = stop_after_attempt(3),
retry = retry_if_result(is.null),
retry_error_callback = function(state) "gave up"
) %@% function() NULL
poll()
#> [1] "gave up"
| Strategy | Functions |
|---|---|
Stop (stop) |
stop_never(), stop_after_attempt(), stop_after_delay(), stop_before_delay(), stop_any(), stop_all()
|
Wait (wait) |
wait_none(), wait_fixed(), wait_rand(), wait_exp(), wait_exp_jitter(), wait_chain(), wait_combine() or a + b
|
Retry (retry) |
retry_all(), retry_never(), retry_if_error(), retry_if_error_message(), retry_if_result()
|
| Hooks |
before_sleep_none(), before_sleep_log(), after_sleep_none(), after_sleep_log()
|
slow_square := lru_cache() %@% function(x) {
Sys.sleep(0.2)
x^2
}
slow_square(4) # slow
slow_square(4) # cached
cache is the same without a size limit. It takes no parameters, so apply it
directly:
slow_cube := cache %@% function(x) {
Sys.sleep(0.2)
x^3
}
disk_cache() keeps results in files under dir, so they survive the session
and can be shared between sessions. dir has no default, because a temporary
directory would defeat the point:
slow_cube := disk_cache("~/.cache/my-analysis") %@% function(x) {
Sys.sleep(0.2)
x^3
}
Set format = "qs2" to use the qs2 package
instead of saveRDS(). On a 1,000,000-row data frame it was about 7x faster to
write and 3.5x faster to read back, and slightly smaller on disk (one machine, so
measure your own). qs2 is optional and must be installed.
Editing a function's body invalidates its entries, but changing something it
calls does not. Results are saved with saveRDS(), so environments and external
pointers do not round-trip.
R has no yield, so a context manager calls teardown() right after acquiring
each resource and returns the resource. Unlike retry() and lru_cache(),
contextmanager takes no parameters, so it is applied without calling it first.
local_tempdir := contextmanager %@% function(pattern = "tmp") {
path <- tempfile(pattern)
dir.create(path)
teardown(unlink(path, recursive = TRUE))
path
}
That one definition gives three forms. Called inside a function, cleanup runs
when that function exits, like withr::local_*():
f <- function() {
d <- local_tempdir()
writeLines("hi", file.path(d, "x.txt"))
d
}
d <- f()
dir.exists(d)
#> [1] FALSE
with_context() scopes it to a block, passing the value to a function:
with_context(local_tempdir(), function(d) list.files(d))
#> character(0)
as_decorator() runs a function inside the context:
g := as_decorator(local_tempdir) %@% function() "ran inside context"
g()
#> [1] "ran inside context"
Cleanups run last in, first out, and the ones registered before a failing setup step still run. A context manager called from another's setup is cleaned up with the outer one.
validate_args() takes one validator per argument and validate_return() takes
one for the result. A validator is any function (or formula) that returns TRUE
to pass. Returning a string fails with that string as the message, which is how
checkmate's check_*() functions behave, so they work without garnish depending
on them:
scale_by := validate_args(x = is.numeric, n = ~ .x > 0) %@% function(x, n = 1) {
x * n
}
scale_by(2, 3)
#> [1] 6
scale_by("a", -1)
#> Error: Invalid arguments.
#> ✖ `x`: failed validation.
#> ✖ `n`: failed validation.
All failures are reported together as a garnish_validation_error. Defaults are
validated too.
Vignettes: vignette("retry"), vignette("caching") and
vignette("contextmanager") and vignette("validation"). The help pages are
?retry, ?lru_cache (which also covers cache and disk_cache),
?contextmanager and ?validate_args. See the adorn
documentation for how decorators are
applied, stacked and removed.