-
Notifications
You must be signed in to change notification settings - Fork 273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Recursive map over nested lists #720
Comments
To make this fully featured one would ideally need two versions one pre and one post visiting. See clojure's walk functionality. With examples postwalk-demo and prewalk-demo. |
I think this is out of scope for purrr just because it's so hard to get the general pattern right. In my experience, there's been relatively little code in common across the places I've had to recursively walk across a list. |
I think something like Haskell's |
This is my current basic implementation. All one needs is a pre, post and leaf mappers for a fully generic case. What is missing is one extra argument for the "leaf" check and more care with attributes. #' @details `rmap()`: is a recursive version of map which applies traverses a
#' nested tree in the depth-first-order and applies `.fpre` to each node on
#' the way down and `.fpost` on the way up and `.fleaf` to non-recursive leafs.
#' @rdname programming
#' @param .x a recursive object
#' @param .fpre,.fpost,.fleaf purrr style functions applied during down-ward
#' (.fpre) and up-ward (.fpost) traversal of the tree. .fleaf is applied to
#' tails only. All default to `identity`.
#' @param ... extra parameters supplied to `.fpre` and `.fpost` and .fleaf.
#' @export
rmap <- function(.x, .fpre = NULL, .fpost = NULL, .fleaf = NULL, ...) {
.fpost <- if (is.null(.fpost)) identity
else purrr::as_mapper(.fpost, ...)
.fpre <- if (is.null(.fpre)) identity
else purrr::as_mapper(.fpre, ...)
.fleaf <- if(is.null(.fleaf)) identity
else purrr::as_mapper(.fleaf, ...)
worker <- function(x) {
x <- .fpre(x)
if (is.recursive(x) && !(is.language(x) || is.function(x))) {
y <- lapply(x, worker)
attributes(y) <- attributes(x)
.fpost(y)
} else {
.fpost(.fleaf(x))
}
}
.x <- .fpre(.x)
.y <- lapply(.x, worker)
attributes(.y) <- attributes(.x)
invisible(.fpost(.y))
} |
(FWIW I'm pretty sure you don't want |
Sure. This is why I check for language and function in the above implementation 😉 A better option is to have a custom rmap <- function(.x, .fpre = NULL, .fpost = NULL, .fleaf = NULL, ...,
is_leaf = function(x) {
is.recursive(x) && !(is.language(x) || is.function(x))
}) {
.fpost <- if (is.null(.fpost)) identity
else purrr::as_mapper(.fpost, ...)
.fpre <- if (is.null(.fpre)) identity
else purrr::as_mapper(.fpre, ...)
.fleaf <- if(is.null(.fleaf)) identity
else purrr::as_mapper(.fleaf, ...)
worker <- function(x) {
x <- .fpre(x)
if (is_leaf(x)) {
.fpost(.fleaf(x))
} else {
y <- lapply(x, worker)
attributes(y) <- attributes(x)
.fpost(y)
}
}
.x <- .fpre(.x)
.y <- lapply(.x, worker)
attributes(.y) <- attributes(.x)
invisible(.fpost(.y))
} As you see it's not that complicated. I hope you could reconsider adding this into the package. One way or another I end up needing this in most of my non-trivial projects. |
Can you show me a few examples of how you use it? I'm still not convinced that this level of generality is at a good place on the compactness vs readability spectrum. |
Whenever one needs to alter selectively elements of a nested structure (jsons, shiny UI elements etc) this function is pretty much the only way to go. Three examples from my recent projects:
clean_recipe <- function(rp) {
rp$steps <-
rmap(rp$steps, .fleaf = function(x) {
if (!is.null(attr(x, ".Environment"))) {
attr(x, ".Environment") <- .GlobalEnv
}
x
})
if (!is.null(rp$template))
rp$template <- head(rp$template, 10)
rp
}
out <- tabsetPanel(...)
out$children[[1]] <-
rmap(out$children[[1]], .fpost = function(x) {
if (is.list(x) && identical(x$name, "a"))
x$attribs$class <- paste("nav-link", x$attribs$class)
x
})
## recursive sort
rsort <- function(x) {
rmap(x, .fpost = function(el) {
if (is.data.table(el))
el <- as.data.frame(el)
if (!is.null(names(el)))
el <- el[sort(names(el))]
if (is.list(el) && !is.data.frame(el))
el <- discard(el, ~ is_empty(.) || (is.data.frame(.) && nrow(.) == 0))
el
})
} |
3rd example on json is something I'd end up doing quite frequently; though not necessarily on json, since any nested list (like sexp, yaml, file system direcatories) could be recursed similarly. |
I think I don't find clean_recipe <- function(rp) {
rp$steps <- clean_recipe_steps(rp$steps)
if (!is.null(rp$template))
rp$template <- head(rp$template, 10)
rp
}
clean_recipe_steps <- function(x) {
if (is.list(x)) {
x[] <- map(x, clean_recipe_steps)
} else if (is.atomic(x)) {
if (!is.null(attr(x, ".Environment"))) {
attr(x, ".Environment") <- .GlobalEnv
}
x
} else {
x
}
}
# -------------------------
out <- tabsetPanel(...)
out$children[[1]] <- add_nav_link(out$children[[1]])
add_nav_link <- function(x) {
if (is.list(x)) {
x[] <- map(x, add_nav_link)
if (identical(x$name, "a"))
x$attribs$class <- paste("nav-link", x$attribs$class)
} else {
x
}
}
# --------------------------
rsort <- function(x) {
if (is.atomic(x)) {
return(x)
}
if (is.data.table(x))
x <- as.data.frame(x)
if (!is.null(names(x)))
x <- x[sort(names(x))]
if (inherits(x, "list")) {
x[] <- map(x, rsort)
x <- discard(x, ~ NROW(.x) == 0)
}
x
} I think I find the other map functions more compelling because they remove more boilerplate that's the same from problem to problem. Every recursion problem is a little different so I feel like making the repeated code invisible makes it harder to understand overall. |
I rewrote rmodify <- function(
.x,
.fpre = identity,
.fpost = identity,
.fleaf = identity, ...,
is_leaf = is_recursive
) {
.fpost <- .fpost %||% purrr::as_mapper(.fpost, ...)
.fpre <- .fpre %||% purrr::as_mapper(.fpre, ...)
.fleaf <- .fleaf %||% purrr::as_mapper(.fleaf, ...)
worker <- function(x) {
out <- .fpre(x)
if (is_leaf(out)) {
out <- .fleaf(x)
} else {
out <- modify(f, worker)
}
.fpost(out)
}
out <- .fpre(.x)
out <- modify(out, worker)
.fpost(out)
}
is_recursive <- function(x) {
inherits(x, "list") || is.pairlist(x) || is.expression(x) || is.call(x)
} |
The
I see your point, but my experience is different. Reading
Indeed. It seems to fit better into midify family functions. |
I don't disagree, but my gut feeling is that the proportion of R programmers that this is true for is relatively small, small enough that it doesn't feel like quite the right fit for purrr. That said, the cost for including it is low, but we are currently on "simplifying purrr" cycle which makes me reluctant to add it now. |
If we can't have Haskell's fold/reduce, could we please have fmap? fmap shouldn't overcomplicate the interface or introduce much maintenance burden. |
@hyiltiz if you want that I'd recommend starting with an issue that describes exactly what |
The While it could also be applied to other structures like vectors, tibbles and matrices, what |
A common scenario for me is to map over nested lists. AFAIK there is no built in or purr functionality for this.
Basically an rapply version but which would operate not just on leaves but also on the inner nodes.
Example:
rmap(sessionInfo(), unclass)
The text was updated successfully, but these errors were encountered: