1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
|
tryCatch <- function(expr, ..., finally) {
base::tryCatch(
withCallingHandlers(
expr,
error = function(e) {
promiseDomain$onError(e)
}
),
...,
finally = finally
)
}
spliceOnFinally <- function(onFinally) {
list(
onFulfilled = finallyToFulfilled(onFinally),
onRejected = finallyToRejected(onFinally)
)
}
finallyToFulfilled <- function(onFinally) {
force(onFinally)
function(value, .visible) {
onFinally()
if (.visible)
value
else
invisible(value)
}
}
finallyToRejected <- function(onFinally) {
force(onFinally)
function(reason) {
onFinally()
stop(reason)
}
}
promiseDomain <- list(
onThen = function(onFulfilled, onRejected, onFinally) {
force(onFulfilled)
force(onRejected)
force(onFinally)
# Verify that if onFinally is non-NULL, onFulfilled and onRejected are NULL
if (!is.null(onFinally) && (!is.null(onFulfilled) || !is.null(onRejected))) {
stop("A single `then` call cannot combine `onFinally` with `onFulfilled`/`onRejected`")
}
# TODO: All wrapped functions should also be rewritten to reenter the domain
# jcheng 2019-07-26: Actually, this seems not to be necessary--the domain
# is getting reentered during callbacks. But I can't figure out now how it's
# happening.
domain <- current_promise_domain()
shouldWrapFinally <- !is.null(onFinally) && !is.null(domain) && !is.null(domain$wrapOnFinally)
newOnFinally <- if (shouldWrapFinally) {
domain$wrapOnFinally(onFinally)
} else {
onFinally
}
if (!is.null(newOnFinally)) {
spliced <- spliceOnFinally(newOnFinally)
onFulfilled <- spliced$onFulfilled
onRejected <- spliced$onRejected
}
shouldWrapFulfilled <- !is.null(onFulfilled) && !is.null(domain) && !shouldWrapFinally
shouldWrapRejected <- !is.null(onRejected) && !is.null(domain) && !shouldWrapFinally
results <- list(
onFulfilled = if (shouldWrapFulfilled) domain$wrapOnFulfilled(onFulfilled) else onFulfilled,
onRejected = if (shouldWrapRejected) domain$wrapOnRejected(onRejected) else onRejected
)
results[!vapply(results, is.null, logical(1))]
},
onError = function(error) {
domain <- current_promise_domain()
if (is.null(domain))
return()
domain$onError(error)
}
)
globals <- new.env(parent = emptyenv())
current_promise_domain <- function() {
globals$domain
}
#' Promise domains
#'
#' Promise domains are used to temporarily set up custom environments that
#' intercept and influence the registration of callbacks. Create new promise
#' domain objects using `new_promise_domain`, and temporarily activate a promise
#' domain object (for the duration of evaluating a given expression) using
#' `with_promise_domain`.
#'
#' While `with_promise_domain` is on the call stack, any calls to [then()] (or
#' higher level functions or operators, like [catch()] or the various [pipes])
#' will belong to the promise domain. In addition, when a `then` callback that
#' belongs to a promise domain is invoked, then any new calls to `then` will
#' also belong to that promise domain. In other words, a promise domain
#' "infects" not only the immediate calls to `then`, but also to "nested" calls
#' to `then`.
#'
#' For more background, read the
#' [original design doc](https://gist.github.com/jcheng5/b1c87bb416f6153643cd0470ac756231).
#'
#' For examples, see the source code of the Shiny package, which uses promise
#' domains extensively to manage graphics devices and reactivity.
#'
#' @param domain A promise domain object to install while `expr` is evaluated.
#' @param expr Any R expression, to be evaluated under the influence of
#' `domain`.
#' @param replace If `FALSE`, then the effect of the `domain` will be added
#' to the effect of any currently active promise domain(s). If `TRUE`, then
#' the current promise domain(s) will be ignored for the duration of the
#' `with_promise_domain` call.
#'
#' @export
with_promise_domain <- function(domain, expr, replace = FALSE) {
oldval <- current_promise_domain()
if (replace)
globals$domain <- domain
else
globals$domain <- compose_domains(oldval, domain)
on.exit(globals$domain <- oldval)
if (!is.null(domain))
domain$wrapSync(expr)
else
force(expr)
}
# Like with_promise_domain, but doesn't include the wrapSync call.
reenter_promise_domain <- function(domain, expr, replace = FALSE) {
oldval <- current_promise_domain()
if (replace)
globals$domain <- domain
else
globals$domain <- compose_domains(oldval, domain)
on.exit(globals$domain <- oldval)
force(expr)
}
#' @param wrapOnFulfilled A function that takes a single argument: a function
#' that was passed as an `onFulfilled` argument to [then()]. The
#' `wrapOnFulfilled` function should return a function that is suitable for
#' `onFulfilled` duty.
#' @param wrapOnRejected A function that takes a single argument: a function
#' that was passed as an `onRejected` argument to [then()]. The
#' `wrapOnRejected` function should return a function that is suitable for
#' `onRejected` duty.
#' @param wrapSync A function that takes a single argument: a (lazily evaluated)
#' expression that the function should [force()]. This expression represents
#' the `expr` argument passed to [with_promise_domain()]; `wrapSync` allows
#' the domain to manipulate the environment before/after `expr` is evaluated.
#' @param onError A function that takes a single argument: an error. `onError`
#' will be called whenever an exception occurs in a domain (that isn't caught
#' by a `tryCatch`). Providing an `onError` callback doesn't cause errors to
#' be caught, necessarily; instead, `onError` callbacks behave like calling
#' handlers.
#' @param ... Arbitrary named values that will become elements of the promise
#' domain object, and can be accessed as items in an environment (i.e. using
#' `[[` or `$`).
#' @param wrapOnFinally A function that takes a single argument: a function
#' that was passed as an `onFinally` argument to [then()]. The
#' `wrapOnFinally` function should return a function that is suitable for
#' `onFinally` duty. If `wrapOnFinally` is `NULL` (the default), then the
#' domain will use both `wrapOnFulfilled` and `wrapOnRejected` to wrap the
#' `onFinally`. If it's important to distinguish between normal
#' fulfillment/rejection handlers and finally handlers, then be sure to
#' provide `wrapOnFinally`, even if it's just [base::identity()].
#' @rdname with_promise_domain
#' @export
new_promise_domain <- function(
wrapOnFulfilled = identity,
wrapOnRejected = identity,
wrapSync = force,
onError = force,
...,
wrapOnFinally = NULL
) {
list2env(list(
wrapOnFulfilled = wrapOnFulfilled,
wrapOnRejected = wrapOnRejected,
wrapOnFinally = wrapOnFinally,
wrapSync = wrapSync,
onError = onError,
...
), parent = emptyenv())
}
compose_domains <- function(base, new) {
if (is.null(base)) {
return(new)
}
list(
wrapOnFulfilled = function(onFulfilled) {
# Force eager evaluation of base$wrapOnFulfilled(onFulfilled)
base <- base$wrapOnFulfilled(onFulfilled)
new$wrapOnFulfilled(base)
},
wrapOnRejected = function(onRejected) {
# Force eager evaluation of base$wrapOnRejected(onRejected)
base <- base$wrapOnRejected(onRejected)
new$wrapOnRejected(base)
},
# Only include the new wrapSync, assuming that we've already applied the
# base domain's wrapSync. This assumption won't hold if we either export
# compose_domains in the future, or if we use it in cases where the base
# domain isn't currently active.
wrapSync = new$wrapSync,
onError = function(e) {
base$onError(e)
new$onError(e)
}
)
}
without_promise_domain <- function(expr) {
with_promise_domain(NULL, expr, replace = TRUE)
}
|