title: Map Raggedly to Depth but Stop at Sentinel
body: <h1>Motivation</h1>
  <p>I am developing an R package with an essential helper function &quot;<code>make()</code>&quot;.  This <code>make()</code> function accepts a ragged list, then maps function <code>foo()</code> onto the <em>penultimate</em> nodes (&quot;twigs&quot;) in the list: those whose children are leaves.  So far <a href="https://purrr.tidyverse.org/reference/map_if.html" rel="nofollow noreferrer"><code>purrr::map_depth</code></a><code>(..., .depth = -2, .ragged = TRUE)</code> seems ideal.</p>
  <p>There is one catch: the traversal must stop at a node that is a <a href="https://rlang.r-lib.org/reference/done.html" rel="nofollow noreferrer"><code>done_box</code></a> — or some sentinel class of my own creation — <em>regardless of its depth</em>, even though that node is also (ie. inherits from) a <code>list</code> of arbitrary depth.  The function <code>foo()</code> will then map the node conditionally, based on its class.</p>
  <h2>Challenge</h2>
  <p>Unfortunately, the inflexibility of <a href="https://purrr.tidyverse.org/reference/vec_depth.html" rel="nofollow noreferrer"><code>purrr::vec_depth()</code></a> induces an error when it encounters objective nodes:</p>
  <pre><code>Error in `.f()`:
  ! `x` must be a vector
  Run `rlang::last_error()` to see where the error occurred.
  </code></pre>
  <hr />
  <h1>Bandaid Solution</h1>
  <p>As such, I have tentatively written a variation <code>.map_depth()</code> on the <a href="https://github.com/tidyverse/purrr/blob/ae4d9ebdf1d2349ac17f590fc7f1ec0fef8c66a8/R/map.R#L285-L295" rel="nofollow noreferrer">source code for <code>map_depth()</code></a>, where <code>.list_depth()</code> replaces <code>vec_depth()</code> to handle objective nodes.  See the <strong>Code</strong> section for the code.</p>
  <p>I can then create a sentinel class <code>my_sentinel</code>, which &quot;boxes&quot; (ie. wraps in a <code>list</code>) a <a href="https://rlang.r-lib.org/reference/topic-quosure.html" rel="nofollow noreferrer"><code>quosure</code></a> for the <code>expression</code> that generates the node's value.  Since this <code>my_sentinel</code> inherits from <code>list</code>, then <code>.list_depth()</code> will give its <code>quosure</code> a depth of <code>1</code> and give the sentinel itself a depth of <code>2</code>.</p>
  <p>At a <code>.depth</code> of <code>-2</code>, the <code>.map_depth()</code> function will thus target the <code>my_sentinel</code> node itself.  By design, <code>foo()</code> will identify its class as <code>&quot;my_sentinel&quot;</code>, and it will evaluate the <code>quosure</code> to &quot;unbox&quot; the value.</p>
  <h2>Drawback</h2>
  <p>Unfortunately, the <code>environment</code> of the <code>quosure</code> may change since the <code>quosure</code> was first captured.  So when <code>.map_depth()</code> is finally called, it might &quot;unbox&quot; the wrong value: one that was wrongly updated.</p>
  <p> As such, the behavior could prove unstable for end users! </p>
  <hr />
  <h1>Question</h1>
  <p>Is there a cleaner or <em>canonical</em> way to <code>purrr::map_*()</code> a function <code>foo()</code> to the &quot;twigs&quot; (ie. nodes at <code>.depth = -2</code>) of a <code>list</code>, while stopping at nodes that are sentinel objects like <code>done_box</code>?</p>
  <h2>Suggestion</h2>
  <p>I'm curious about <a href="https://purrr.tidyverse.org/reference/map_if.html" rel="nofollow noreferrer"><code>purrr::map_if()</code></a>, with some predicate <code>.p</code> that tests for both class and depth.  However, I lack the expertise to confirm this as feasible, let alone <em>canonical</em>.</p>
  <hr />
  <h1>Code</h1>
  <p>For <code>my_sentinel</code>:</p>
  <pre><code>my_sentinel &lt;- function(x) {
    x_quo &lt;- rlang::enquo0(x)
    
    # No pun intended.
    x_box &lt;- list(x_quo)
    
    structure(x_box,
      class = c(&quot;my_sentinel&quot;, class(x_box))
    )
  }
  </code></pre>
  <p>For <code>.map_depth()</code>:</p>
  <pre><code># A variation on 'purrr::map_depth()' that accommodates objective leaves.
  .map_depth &lt;- function(.x, .depth, .f, ..., .ragged = FALSE) {
    if (!rlang::is_integerish(.depth, n = 1, finite = TRUE)) {
      abort(&quot;`.depth` must be a single number&quot;)
    }
    if (.depth &lt; 0) {
      .depth &lt;- .list_depth(.x) + .depth
    }
    .f &lt;- 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 &lt;- function(.x, .depth, .f, ..., .ragged, .atomic) {
    if (.depth &lt; 0) {
      rlang::abort(&quot;Invalid depth&quot;)
    }
    # TODO: Must this be addressed too (unlikely)?
    if (.atomic) {
      if (!.ragged) {
        rlang::abort(&quot;List not deep enough&quot;)
      }
      return(purrr::map(.x, .f, ...))
    }
    if (.depth == 0) {
      return(.f(.x, ...))
    }
    if (.depth == 1) {
      return(purrr::map(.x, .f, ...))
    }
    .atomic &lt;- 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 &lt;- 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 &lt;- 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(&quot;`x` must be a vector or an object&quot;)
    }
  }
  </code></pre>