Motivation
I am developing an R package with an essential helper function "make()". This make() function accepts a ragged list, then maps function foo() onto the penultimate nodes ("twigs") in the list: those whose children are leaves. So far purrr::map_depth(..., .depth = -2, .ragged = TRUE) seems ideal.
There is one catch: the traversal must stop at a node that is a done_box — or some sentinel class of my own creation — regardless of its depth, even though that node is also (ie. inherits from) a list of arbitrary depth. The function foo() will then map the node conditionally, based on its class.
Challenge
Unfortunately, the inflexibility of purrr::vec_depth() induces an error when it encounters objective nodes:
Error in `.f()`:
! `x` must be a vector
Run `rlang::last_error()` to see where the error occurred.
Bandaid Solution
As such, I have tentatively written a variation .map_depth() on the source code for map_depth(), where .list_depth() replaces vec_depth() to handle objective nodes. See the Code section for the code.
I can then create a sentinel class my_sentinel, which "boxes" (ie. wraps in a list) a quosure for the expression that generates the node's value. Since this my_sentinel inherits from list, then .list_depth() will give its quosure a depth of 1 and give the sentinel itself a depth of 2.
At a .depth of -2, the .map_depth() function will thus target the my_sentinel node itself. By design, foo() will identify its class as "my_sentinel", and it will evaluate the quosure to "unbox" the value.
Drawback
Unfortunately, the environment of the quosure may change since the quosure was first captured. So when .map_depth() is finally called, it might "unbox" the wrong value: one that was wrongly updated.
As such, the behavior could prove unstable for end users!
Question
Is there a cleaner or canonical way to purrr::map_*() a function foo() to the "twigs" (ie. nodes at .depth = -2) of a list, while stopping at nodes that are sentinel objects like done_box?
Suggestion
I'm curious about purrr::map_if(), with some predicate .p that tests for both class and depth. However, I lack the expertise to confirm this as feasible, let alone canonical.
Code
For my_sentinel:
my_sentinel <- function(x) {
x_quo <- rlang::enquo0(x)
# No pun intended.
x_box <- list(x_quo)
structure(x_box,
class = c("my_sentinel", class(x_box))
)
}
For .map_depth():
# A variation on 'purrr::map_depth()' that accommodates objective leaves.
.map_depth <- function(.x, .depth, .f, ..., .ragged = FALSE) {
if (!rlang::is_integerish(.depth, n = 1, finite = TRUE)) {
abort("`.depth` must be a single number")
}
if (.depth < 0) {
.depth <- .list_depth(.x) + .depth
}
.f <- purrr::as_mapper(.f, ...)
.map_depth_rec(.x, .depth, .f, ..., .ragged = .ragged, .atomic = FALSE)
}
# A variation on 'purrr:::map_depth_rec()' that accommodates objective leaves.
.map_depth_rec <- function(.x, .depth, .f, ..., .ragged, .atomic) {
if (.depth < 0) {
rlang::abort("Invalid depth")
}
# TODO: Must this be addressed too (unlikely)?
if (.atomic) {
if (!.ragged) {
rlang::abort("List not deep enough")
}
return(purrr::map(.x, .f, ...))
}
if (.depth == 0) {
return(.f(.x, ...))
}
if (.depth == 1) {
return(purrr::map(.x, .f, ...))
}
.atomic <- rlang::is_atomic(.x)
purrr::map(.x, function(x) {
.map_depth_rec(x, .depth - 1, .f, ..., .ragged = .ragged,
.atomic = .atomic)
})
}
# A variation on 'purrr::vec_depth()' that accommodates objective leaves.
.list_depth <- function(x) {
if (rlang::is_null(x)) {
0L
}
# TODO: Address this so a vector is treated as a leaf (or must users esc() for that?).
else if (rlang::is_atomic(x)) {
1L
}
else if (rlang::is_list(x)) {
depths <- purrr::map_int(x, .list_depth)
1L + max(depths, 0L)
}
# Accommodate objective leaves.
else if (is.object(x)) {
# TODO: Check if this should be 1L or (as I suspect) 0L!
1L
}
else {
rlang::abort("`x` must be a vector or an object")
}
}
Just mentioning
rrapply()in packagerrapply(an extended version of baserapply), which may already provide the functionality you are looking for.Using the following dummy list, since no data is provided in the question:
In a first call, the
my_sentinalclass is recursively propagated to each sublist. In a second call, some functionfis applied to all lists with no sublists that do not inherit from themy_sentinalclass. NB: this can probably be combined into a single call with some effort, but split into two separate calls the code is likely easier to follow and understand.Disclaimer: I am also the maintainer of the
rrapply-package.