Passing tidy select arguments to inner functions while maintaining "missingness"

Viewed 139

Can someone help me figure out the canonical "tidyeval" way to solve this:

  • outer() takes a data frame data and an optional(!) tidy selection argument var.
  • var is passed on to inner()
  • inner() only performs select() if var is not missing
  • Requirement: the signature of inner() and outer() should be the same, since inner() might be called independently

Here's my approach, but it isn't working. Problem: missing(var) returns TRUE in outer() but FALSE in inner().

inner <- function(data, var) {
  print(missing(var))
  if (missing(var)) data else dplyr::select(data, {{ var }})
}

outer <- function(data, var) {
  print(missing(var))
  inner(data, !!rlang::enquo(var))     
}

outer(mtcars)

#> [1] TRUE
#> [1] FALSE
#> data frame with 0 columns and 6 rows

inner(mtcars)

#> [1] TRUE
#>                    mpg cyl disp  hp drat    wt  qsec vs am gear carb
#> Mazda RX4         21.0   6  160 110 3.90 2.620 16.46  0  1    4    4
#> Mazda RX4 Wag     21.0   6  160 110 3.90 2.875 17.02  0  1    4    4
#> Datsun 710        22.8   4  108  93 3.85 2.320 18.61  1  1    4    1
#> Hornet 4 Drive    21.4   6  258 110 3.08 3.215 19.44  1  0    3    1
#> Hornet Sportabout 18.7   8  360 175 3.15 3.440 17.02  0  0    3    2
#> Valiant           18.1   6  225 105 2.76 3.460 20.22  1  0    3    1
...

My naive solution would be to check missing(var) in outer and change the call to inner() accordingly (see below), but I wonder if there is a way to just pass on the var argument without any such checks?

# works as desired
outer <- function(data, var) {
  print(missing(var))
  if (missing(var)) inner(data) else inner(data, !!rlang::enquo(var))
}
2 Answers

One way is to give the adequate default argument to var and unconditionally pass it to select():

inner <- function(data, var = everything()) {
  dplyr::select(data, {{ var }})
}
outer <- function(data, var = everything()) {
  inner(data, {{ var }})
}

names(outer(mtcars))
#>  [1] "mpg"  "cyl"  "disp" "hp"   "drat" "wt"   "qsec" "vs"   "am"   "gear" "carb"

names(outer(mtcars, starts_with("d")))
#> [1] "disp" "drat"

For more complex cases (hopefully not needed) you need to disentangle the !! and enquo() steps which are merged in a single {{ operation. Then you can examine the defused quosure:

inner <- function(data, var) {
  var <- enquo(var)
  if (quo_is_missing(var)) {
    data
  } else {
    dplyr::select(data, !!var)
  }
}
outer <- function(data, var) {
  inner(data, {{ var }})
}

Ah, sorry! I don't know why I thought the !!enquo() was necessary. It turns out that passing on the optional argument without changing its "missingness" is as simple as this:

inner <- function(data, var) {
  print(missing(var))
  if (missing(var)) {
    print(colnames(data))
  } else {
    print(colnames(dplyr::select(data, {{ var }})))
  }
}

outer <- function(data, var) {
  print(missing(var))
  inner(data, var)
}

outer(mtcars, dplyr::starts_with("m"))

#> [1] FALSE
#> [1] FALSE
#> [1] "mpg"

outer(mtcars)

#> [1] TRUE
#> [1] TRUE
#> [1] "mpg"  "cyl"  "disp"  "hp"  "drat" "wt" "qsec" "vs" "am"  "gear" "carb"

Edit: show how to query missingness and apply tidy selection if the argument isn't missing.

Related