metaprogramming
metaprogramming.Rmd
metayer provides two methods for easing metaprogramming tasks,
wrapped_factory
and with_monkey_patch
.
wrapped_factory
It’s easier to start with wrapped_factory
which will
allow us to wrap existing functions. One feature of
wrapped_factory
is that it replicates the wrapped
function’s signature, including default values and parameters that would
otherwise be lazily evaluated.
We note that wrappers should adopt the form
function(cmd, args, ...)
. wrapped_factory
will
substitute cmd
and args
in the wrapped
function. ...
may be replaced with key-value pairs which
will be available in the wrapper scope.
An example should be illustrative. Let’s define a wrapper that adds printf debugging to an existing function.
# a simple "debug" wrapper
debug_wrapper <- function(cmd, args, label = NULL) {
# emit debugging information
sprintf(">>> called '%s'\n", label) %>%
cat(file = stdout())
# call the original function
do.call(cmd, args)
}
We’ll apply this wrapper to a simple function:
# a very simple function
sum <- function(x, y) x + y
Now, invoke the wrapped_factory
machinery to produce a
wrapped function. This will print the debugging information on stdout
and the return the result.
dbg_sum <- wrapped_factory("sum", debug_wrapper, label = "..sum..")
dbg_sum(1, 2)
>>> called '..sum..'
[1] 3
It’s instructive to inspect the structure of dbg_sum
. It
has the same function signature as the original function; the body of
the function is debug_wrapper
with adapted cmd
and args
. Specifically, cmd
has been replaced
with sum
, and args
has been replaced with a
symbol-mapped list. wrapped_factory
makes these changes
with the substitute
function, and these replacements will
happen wherever these symbols are found in the wrapper code.
dbg_sum
function (x, y)
{
sprintf(">>> called '%s'\n", label) %>% cat(file = stdout())
do.call(sum, list(x = x, y = y))
}
<environment: 0x59544662c038>
with_monkey_patch
with_monkey_patch
does roughly the same thing as
wrapped_factory
, but it is applied to functions defined in
a namespace and only temporarily. Changes will be restored on exit.
The following is a simple example. Consider the behavior of “base::Sys.time”: it captures the local time zone in its output.
# e.g., "PDT" in Seattle.
Sys.time()
[1] "2024-10-09 05:36:15 PDT"
That means that a function, like the one below, is inherently broken. It’s an inflexible implementation, only meaningful in a single, fixed timezone determined apriori.
# hmm...
get_time_string <- function() {
Sys.time() %>% as.character()
}
get_time_string()
[1] "2024-10-09 05:36:15.343231"
with_monkey_patch
allows us to to fix
get_time_string
indirectly. It does this by temporarily
modifying the behavior of base::Sys.time
in a scoped block
of client code. Here, calls to base::Sys.time
will return
UTC.
# changes the behavior of base::Sys.time so that it returns UTC
with_monkey_patch(
"base::Sys.time",
#
wrapper = function(cmd, args, func) {
t <- do.call(func, args)
.POSIXct(t, "UTC")
},
{
# scoped block: (nested) calls to Sys.time only take effect here
get_time_string()
}
)
[1] "2024-10-09 12:36:15.406157"
# check that the original behavior is restored
get_time_string()
[1] "2024-10-09 05:36:15.454998"
a real example
While the above would hopefully never happen in production code, there are examples where the monkey patch machinery is useful for modifying an object created deep in a call stack.
metayer initially used this monkey patching pattern to affix knitr
hooks to rmarkdown documents. Specifically,
pkgdown::build_article
invokes
rmarkdown::html_document
, but does so deep in the call
stack and with limited configurability. Our scenario requires a knitr
hook to be installed, and while it is trivial to modify an html_document
object for this purpose, access to this object wasn’t exposed through
the pkgdown API. One proposal was to monkey patch the behavior of
rmarkdown::html_document
so that it included the
post-processing steps that were required.
Ultimately, the monkey patching approach for
rmarkdown::html_document
was refactored away, but, for a
time, it provided a viable solution to the original problem.