Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

recursive purrr::modify_depth() does not work on ragged lists

Say, this is my somewhat convoluted and ragged list:

res <- list(
  a = TRUE,
  b = "error msg 1",
  c = list(
    TRUE,
    "error msg 2"
  ),
  d = list(
    e = "error msg 3",
    "error msg 4",  # no name for this list item just to make things interesting
    f = list(
      g = list(
        h = "error msg 5",
        i = TRUE
      )
    )
  )
)

I would now like to, say, apply some function at 2 depth (from the top). My list can be arbitrarily deep and ragged.

I want to be all cool and tidyvers-y, so I though this would work:

purrr::modify_depth(.x = res, .depth = 2, .f = str, .ragged = TRUE)

But that, unexpectedly, fails with

Error in .x[] <- .f(.x, ...) : replacement has length zero

Can't make heads nor tails of this, because when I str() my way through all the list elements manually, it works just fine; str() does always give some result.

I am guessing that I'm using .ragged = wrong.

I'm also noticing that the same setup works, when using is.null() as a function, instead of str(), but is then applied to leaves which don't actually exist (expanding the list).

purrr::modify_depth(.x = res, .depth = 4, .f = is.null, .ragged = TRUE)

This creates a list that is uniformly 4 deep, though the original is actually quite ragged and only 4 deep down 1 branch.

What I'd like to do, is to modify only those list elements for which a n depth actually exists, and to leave all others unmodified.

How can I get purrr::modify_depth() to do that?

like image 317
maxheld Avatar asked Nov 08 '22 10:11

maxheld


1 Answers

This is an old question, but since a satisfying answer is missing here it goes.

First, str displays output to the terminal, but does not actually return anything:

is.null(str("anything"))
#>  chr "anything"
#> [1] TRUE

The purrr-call fails as .f = str tries to assign NULL values to the list elements, which is not allowed. The error message in the current purrr version (3.4.0) also states this more clearly:

purrr::modify_depth(.x = res, .depth = 2, .f = str, .ragged = TRUE)
#>  logi TRUE
#> Error: Result 1 must be a single logical, not NULL of length 0

Replacing str by a different function, purrr no longer complains:

purrr::modify_depth(.x = res, .depth = 2, .f = is.character, .ragged = TRUE)
#> $a
#> [1] FALSE
#> 
#> $b
#> [1] "TRUE"
#> 
#> $c
#> $c[[1]]
#> [1] FALSE
#> 
#> $c[[2]]
#> [1] TRUE
#> 
#> 
#> $d
#> $d$e
#> [1] TRUE
#> 
#> $d[[2]]
#> [1] TRUE
#> 
#> $d$f
#> [1] FALSE

However, the issue remains that modify_depth applies .f exactly at .depth = 2 so any sublists at deeper levels will be collapsed into a logical value by .f = is.character


Another (non-purrr) option could be rrapply in the rrapply-package (extended version of base-rrapply) to recurse through a nested list. Setting how = "replace" we can modify only elements at a specific depth, while keeping all other list elements unmodified:

library(rrapply)

rrapply(res, condition = function(x, .xpos) length(.xpos) == 2, f = function(x) paste(x, "<- modified"), how = "replace")
#> $a
#> [1] TRUE
#> 
#> $b
#> [1] "error msg 1"
#> 
#> $c
#> $c[[1]]
#> [1] "TRUE <- modified"
#> 
#> $c[[2]]
#> [1] "error msg 2 <- modified"
#> 
#> 
#> $d
#> $d$e
#> [1] "error msg 3 <- modified"
#> 
#> $d[[2]]
#> [1] "error msg 4 <- modified"
#> 
#> $d$f
#> $d$f$g
#> $d$f$g$h
#> [1] "error msg 5"
#> 
#> $d$f$g$i
#> [1] TRUE

Here, condition decides to which list elements the f function is applied and the .xpos argument evaluates to the position of the element in the nested list as an integer vector. length(.xpos) can then be used to evaluate the depth of any element in the nested list.

like image 83
Joris C. Avatar answered Nov 15 '22 07:11

Joris C.