简体   繁体   English

Map 参差不齐到深度,但停在哨兵

[英]Map Raggedly to Depth but Stop at Sentinel

Motivation动机

I am developing an R package with an essential helper function " make() ".我正在开发一个 R package 和一个基本的助手 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.这个make() function 接受一个参差不齐的列表,然后将 function foo()映射到列表中的倒数第二个节点(“树枝”)上:那些孩子是叶子的节点。 So far purrr::map_depth (..., .depth = -2, .ragged = TRUE) seems ideal.到目前为止purrr::map_depth (..., .depth = -2, .ragged = TRUE)似乎很理想。

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.有一个问题:遍历必须在一个done_box节点处停止 - 或者我自己创建的一些哨兵 class -无论其深度如何,即使该节点也是(即继承自)任意深度的list The function foo() will then map the node conditionally, based on its class.然后 function foo()将根据其 class 有条件地对节点进行 map。

Challenge挑战

Unfortunately, the inflexibility of purrr::vec_depth() induces an error when it encounters objective nodes:不幸的是, purrr::vec_depth()的不灵活性在遇到目标节点时会引发错误:

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.因此,我暂时在.map_depth()源代码上编写了一个变体 .map_depth map_depth() ,其中.list_depth()替换vec_depth()来处理目标节点。 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.然后,我可以创建一个哨兵quosure my_sentinel ,它为生成节点值的expression “装箱”(即包装在一个list )。 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 .由于此my_sentinel继承自list ,因此.list_depth()将为其quosure提供1的深度,并为哨兵本身提供2的深度。

At a .depth of -2 , the .map_depth() function will thus target the my_sentinel node itself..depth-2时, .map_depth() function 将因此以my_sentinel节点本身为目标。 By design, foo() will identify its class as "my_sentinel" , and it will evaluate the quosure to "unbox" the value.按照设计, foo()会将其quosure标识为"my_sentinel" ,并将评估“取消装箱”该值的条件。

Drawback退税

Unfortunately, the environment of the quosure may change since the quosure was first captured.不幸的是,自从首次捕获quosure quosure environment可能会发生变化。 So when .map_depth() is finally called, it might "unbox" the wrong value: one that was wrongly updated.因此,当最终调用.map_depth()时,它可能会“取消装箱”错误的值:错误更新的值。

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 ?是否有更清洁或规范的方法来purrr::map_*()将 function foo()list的“树枝”(即.depth = -2的节点),同时停在像done_box这样的哨兵对象的节点?

Suggestion建议

I'm curious about purrr::map_if() , with some predicate .p that tests for both class and depth.我很好奇purrr::map_if() ,有一些谓词 .p 可以测试.p和深度。 However, I lack the expertise to confirm this as feasible, let alone canonical .但是,我缺乏确认这是否可行的专业知识,更不用说canonical了。


Code代码

For my_sentinel :对于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() :对于.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 package rrapply (an extended version of base rapply ), which may already provide the functionality you are looking for.只需在 package rrapply (基本rapply的扩展版本rrapply()中提到 rrapply(),它可能已经提供了您正在寻找的功能。

Using the following dummy list, since no data is provided in the question:使用以下虚拟列表,因为问题中没有提供数据:

## dummy data
l <- list(
  list("initial_node"),
  list(list("initial_node")),
  structure(list("initial_node"), class = c("list", "my_sentinal")),
  structure(list(list("initial_node")), class = c("list", "my_sentinal"))
)

In a first call, the my_sentinal class is recursively propagated to each sublist.在第一次调用中, my_sentinal class 递归地传播到每个子列表。 In a second call, some function f is applied to all lists with no sublists that do not inherit from the my_sentinal class.在第二次调用中,一些 function f应用于所有没有继承自my_sentinal class 的子列表的列表。 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.注意:这可能可以通过一些努力组合成一个调用,但分成两个单独的调用,代码可能更容易理解和理解。

## propagate my_sentinal class to deeper sublists
l1 <- rrapply(
  l, 
  classes ="list",
  condition = \(x) inherits(x, "my_sentinal"),
  f = \(x) {
    if(is.list(x[[1]])) x[] <- lapply(x, structure, class = c("list", "my_sentinal"))
    return(x)
  },
  how = "recurse"
)

## apply function to non-sentinal pen-ultimate nodes 
l2 <- rrapply(
  l1, 
  classes = "list",
  condition = \(x) !inherits(x, "my_sentinal") && !is.list(x[[1]]),
  f = \(x) lapply(x, \(xi) "processed_node")
)

str(l2)
#> List of 4
#>  $ :List of 1
#>   ..$ : chr "processed_node"
#>  $ :List of 1
#>   ..$ :List of 1
#>   .. ..$ : chr "processed_node"
#>  $ :List of 1
#>   ..$ : chr "initial_node"
#>   ..- attr(*, "class")= chr [1:2] "list" "my_sentinal"
#>  $ :List of 1
#>   ..$ :List of 1
#>   .. ..$ : chr "initial_node"
#>   .. ..- attr(*, "class")= chr [1:2] "list" "my_sentinal"
#>   ..- attr(*, "class")= chr [1:2] "list" "my_sentinal"

Disclaimer: I am also the maintainer of the rrapply -package.免责声明:我也是rrapply的维护者。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM