Scoped side effects and events for R.
This package:
-
generalizes
on.exit()
to a functiondefer()
, which can attach exit handlers to any currently executing function, and uses that to scope side effects, and -
provides a simple event system, using event handlers registered through
on()
/off()
, and events fired throughemit()
.
The defer()
function generalizes on.exit()
, allowing a function to perform cleanup work after a parent invoking function has finished execution. For example, the following scope_dir(dir)
function can be used to set the working directory for duration of a function's execution:
scope_dir <- function(dir) {
owd <- setwd(dir)
defer_parent(setwd(owd))
}
The scope_dir()
function can be called to set the working directory, and restore the working directory when the caller is done. For example:
in_dir <- function(dir) {
scope_dir(dir)
print(basename(getwd()))
}
print(basename(getwd()))
## [1] "later"
in_dir("tests")
## [1] "tests"
print(basename(getwd()))
## [1] "later"
In effect, defer()
provides a mechanism for mimicing something like C++ destructors -- you can call a function that registers actions that will be run when the enclosing function has finished execution.
Register an event handler using on("event", <handler>)
, and fire events with emit("event", <data>)
. Useful for long-range communication between functions / objects.
These functions are similar to builtin R
capabilities: signalCondition()
allows you to signal a condition, and withCallingHandlers()
can be used to register handlers for signaled conditions (alongside errors, warnings, and otherwise). The main difference between the system in later
and in base R
:
-
Rather than wrapping the executing expression in
withCallingHandlers()
, you can just callon()
anywhere in a function's body, and any events emitted later on in anyR
code will be handled by that handler, -
A call to
emit()
can emit any kind of data object; you are not limited toR
conditions, -
A registered handler for an event won't alter control flow (ie, this event system doesn't have the
restarts
mechanism thatR
uses for 'long jumps'), -
Handlers are registered in a stack (so the most recently registered handlers will handle an event first); and handlers can call
stop_propagation()
to ensure a handler deeper in the stack does not receive the event if desired.
An example to illustrate, with a set of 'workers' that can call emit()
when they've completed some work, and a 'manager' that listens for the emitted events from these workers.
set.seed(1)
make_worker <- function(i) {
force(i)
function() {
emit("work", i)
}
}
workers <- lapply(seq_len(5), make_worker)
manager <- function() {
counter <- 0
on("work", function(data) {
cat(sprintf("Received data: '%s'\n", data), sep = "")
counter <<- counter + 1
})
# Run for 2 milliseconds
time <- Sys.time()
while (Sys.time() - time < 0.002) {
worker <- sample(workers, 1)[[1]]
worker()
}
cat(sprintf("Executed %s tasks.\n", counter), sep = "")
}
manager()
## Received data: '4'
## Received data: '2'
## Received data: '3'
## Received data: '5'
## Received data: '5'
## Received data: '5'
## Received data: '5'
## Received data: '4'
## Received data: '2'
## Received data: '4'
## Executed 10 tasks.
This package is heavily inspired by Jim Hester's withr package.
The main 'trick' that enabled the generalization of on.exit()
was provided in a mailing list post here, by Peter Meilstrup: https://stat.ethz.ch/pipermail/r-devel/2013-November/067874.html