Pass ... to across() within custom function

Hi,

Prior to dplyr 1.0.0, when the scoped variants of verbs had not yet been superseded, I wrote several custom functions that allowed for user-defined arguments to be passed along to dplyr functions. I have been working to update the syntax of some of these functions since the release of dplyr 1.0 (and functions such as across(), if_any(), if_all() etc.), but I've been struggling with updating the function in the reprex below.

The purpose of this function is to allow a user to filter a dataset where all user-selected columns in an observation are NA. The original function does not use dot-dot-dot (...) notation to accept these arguments, but I am trying to make the updated function accept a ..., and use the most recent version of dplyr. Any thoughts on how to do this given the examples?

suppressPackageStartupMessages({
  library(tidyverse)
  library(rlang)
  library(magrittr)
})

example_table <- tibble::tibble(a = c(1, 1, NA, 1), b = c(NA, NA, NA, 1), c = c(2, NA, NA, 2))

example_table
#> # A tibble: 4 × 3
#>       a     b     c
#>   <dbl> <dbl> <dbl>
#> 1     1    NA     2
#> 2     1    NA    NA
#> 3    NA    NA    NA
#> 4     1     1     2

# Old version of dplyr (<1.0.0). This function works.
function_old_dplyr <- function(dataframe, columns) {
  columns <- rlang::enquo(columns)

  dataframe %<>%
    dplyr::filter_at(dplyr::vars(!!columns), dplyr::any_vars(!is.na(.)))

  return(dataframe)
}

# This works
function_old_dplyr(example_table, c(a, b))
#> # A tibble: 3 × 3
#>       a     b     c
#>   <dbl> <dbl> <dbl>
#> 1     1    NA     2
#> 2     1    NA    NA
#> 3     1     1     2



# Attempt with new dplyr (>1.0.0). I
function_new_dplyr <- function(dataframe, ...) {
  columns <- rlang::enquos(...)

  dataframe %<>%
    dplyr::filter(dplyr::across(dplyr::vars(!!!columns), ~ dplyr::any_vars(!is.na(.))))

  return(dataframe)
}

# This doesn't
function_new_dplyr(example_table, a, b)
#> Error: Problem with `filter()` input `..1`.
#> ℹ Input `..1` is `dplyr::across(dplyr::vars(a, b), ~dplyr::any_vars(!is.na(.)))`.
#> x Must subset columns with a valid subscript vector.
#> x Subscript has the wrong type `quosures`.
#> ℹ It must be numeric or character.

Created on 2021-11-05 by the reprex package (v2.0.0)

Standard output and standard error
Error: Can't combine <quosures> and <logical>.
Backtrace:
     █
  1. ├─global::function_new_dplyr(example_table, a, b)
  2. │ └─`%<>%`(...)
  3. ├─dplyr::filter(., dplyr::across(dplyr::vars(!!!columns), ~dplyr::any_vars(!is.na(.))))
  4. ├─dplyr:::filter.data.frame(...)
  5. │ └─dplyr:::filter_rows(.data, ..., caller_env = caller_env())
  6. │   ├─base::withCallingHandlers(...)
  7. │   └─mask$eval_all_filter(dots, env_filter)
  8. ├─dplyr::across(dplyr::vars(a, b), ~dplyr::any_vars(!is.na(.)))
  9. │ └─dplyr:::across_setup(...)
 10. │   └─tidyselect::eval_select(cols, data = across_cols)
 11. │     └─tidyselect:::eval_select_impl(...)
 12. │       ├─tidyselect:::with_subscript_errors(...)
 13. │       │ ├─base::tryCatch(...)
 14. │       │ │ └─base:::tryCatchList(expr, classes, parentenv, handlers)
 15. │       │ │   └─base:::tryCatchOne(expr, names, parentenv, handlers[[1L]])
 16. │       │ │     └─base:::doTryCatch(return(expr), name, parentenv, handler)
 17. │       │ └─tidyselect:::instrument_base_errors(expr)
 18. │       │   └─base::withCallingHandlers(...)
 19. │       └─tidyselect:::vars_select_eval(...)
 20. │         └─tidyselect:::walk_data_tree(expr, data_mask, context_mask)
 21. │           └─tidyselect:::as_indices_sel_impl(...)
 22. │             └─tidyselect:::as_indices_impl(x, vars, strict = strict)
 23. │               └─vctrs::vec_as_subscript(x, logical = "error")
 24. └─vctrs:::try_catch_impl(...)
 25.   ├─base::tryCatch(try_catch_callback(data, NULL), ...)
 26.   │ └─base:::tryCatchList(expr, classes, parentenv, handlers)
 27.   │   └─base:::tryCatchOne(expr, names, parentenv, handlers[[1L]])
 28.   │     └─base:::doTryCatch(return(expr), name, parentenv, handler)
 29.   └─vctrs:::try_catch_callback(data, NULL)
 30.     └─(function () ...
 31.       └─vctrs::vec_default_ptype2(...)
 32.         └─vctrs::stop_incompatible_type(...)
 33.           └─vctrs:::stop_incompatible(...)
 34.             └─vctrs:::stop_vctrs(...)
Error: Can't combine <quosures> and <integer>.
Backtrace:
     █
  1. ├─global::function_new_dplyr(example_table, a, b)
  2. │ └─`%<>%`(...)
  3. ├─dplyr::filter(., dplyr::across(dplyr::vars(!!!columns), ~dplyr::any_vars(!is.na(.))))
  4. ├─dplyr:::filter.data.frame(...)
  5. │ └─dplyr:::filter_rows(.data, ..., caller_env = caller_env())
  6. │   ├─base::withCallingHandlers(...)
  7. │   └─mask$eval_all_filter(dots, env_filter)
  8. ├─dplyr::across(dplyr::vars(a, b), ~dplyr::any_vars(!is.na(.)))
  9. │ └─dplyr:::across_setup(...)
 10. │   └─tidyselect::eval_select(cols, data = across_cols)
 11. │     └─tidyselect:::eval_select_impl(...)
 12. │       ├─tidyselect:::with_subscript_errors(...)
 13. │       │ ├─base::tryCatch(...)
 14. │       │ │ └─base:::tryCatchList(expr, classes, parentenv, handlers)
 15. │       │ │   └─base:::tryCatchOne(expr, names, parentenv, handlers[[1L]])
 16. │       │ │     └─base:::doTryCatch(return(expr), name, parentenv, handler)
 17. │       │ └─tidyselect:::instrument_base_errors(expr)
 18. │       │   └─base::withCallingHandlers(...)
 19. │       └─tidyselect:::vars_select_eval(...)
 20. │         └─tidyselect:::walk_data_tree(expr, data_mask, context_mask)
 21. │           └─tidyselect:::as_indices_sel_impl(...)
 22. │             └─tidyselect:::as_indices_impl(x, vars, strict = strict)
 23. │               └─vctrs::vec_as_subscript(x, logical = "error")
 24. └─vctrs:::try_catch_impl(...)
 25.   ├─base::tryCatch(try_catch_callback(data, NULL), ...)
 26.   │ └─base:::tryCatchList(expr, classes, parentenv, handlers)
 27.   │   └─base:::tryCatchOne(expr, names, parentenv, handlers[[1L]])
 28.   │     └─base:::doTryCatch(return(expr), name, parentenv, handler)
 29.   └─vctrs:::try_catch_callback(data, NULL)
 30.     └─(function () ...
 31.       └─vctrs::vec_default_ptype2(...)
 32.         └─vctrs::stop_incompatible_type(...)
 33.           └─vctrs:::stop_incompatible(...)
 34.             └─vctrs:::stop_vctrs(...)
Error: Can't combine <quosures> and <character>.
Backtrace:
     █
  1. ├─global::function_new_dplyr(example_table, a, b)
  2. │ └─`%<>%`(...)
  3. ├─dplyr::filter(., dplyr::across(dplyr::vars(!!!columns), ~dplyr::any_vars(!is.na(.))))
  4. ├─dplyr:::filter.data.frame(...)
  5. │ └─dplyr:::filter_rows(.data, ..., caller_env = caller_env())
  6. │   ├─base::withCallingHandlers(...)
  7. │   └─mask$eval_all_filter(dots, env_filter)
  8. ├─dplyr::across(dplyr::vars(a, b), ~dplyr::any_vars(!is.na(.)))
  9. │ └─dplyr:::across_setup(...)
 10. │   └─tidyselect::eval_select(cols, data = across_cols)
 11. │     └─tidyselect:::eval_select_impl(...)
 12. │       ├─tidyselect:::with_subscript_errors(...)
 13. │       │ ├─base::tryCatch(...)
 14. │       │ │ └─base:::tryCatchList(expr, classes, parentenv, handlers)
 15. │       │ │   └─base:::tryCatchOne(expr, names, parentenv, handlers[[1L]])
 16. │       │ │     └─base:::doTryCatch(return(expr), name, parentenv, handler)
 17. │       │ └─tidyselect:::instrument_base_errors(expr)
 18. │       │   └─base::withCallingHandlers(...)
 19. │       └─tidyselect:::vars_select_eval(...)
 20. │         └─tidyselect:::walk_data_tree(expr, data_mask, context_mask)
 21. │           └─tidyselect:::as_indices_sel_impl(...)
 22. │             └─tidyselect:::as_indices_impl(x, vars, strict = strict)
 23. │               └─vctrs::vec_as_subscript(x, logical = "error")
 24. └─vctrs:::try_catch_impl(...)
 25.   ├─base::tryCatch(try_catch_callback(data, NULL), ...)
 26.   │ └─base:::tryCatchList(expr, classes, parentenv, handlers)
 27.   │   └─base:::tryCatchOne(expr, names, parentenv, handlers[[1L]])
 28.   │     └─base:::doTryCatch(return(expr), name, parentenv, handler)
 29.   └─vctrs:::try_catch_callback(data, NULL)
 30.     └─(function () ...
 31.       └─vctrs::vec_default_ptype2(...)
 32.         └─vctrs::stop_incompatible_type(...)
 33.           └─vctrs:::stop_incompatible(...)
 34.             └─vctrs:::stop_vctrs(...)
Error: Must subset elements with a valid subscript vector.
x Subscript has the wrong type `quosures`.
ℹ It must be numeric or character.
Backtrace:
     █
  1. ├─global::function_new_dplyr(example_table, a, b)
  2. │ └─`%<>%`(...)
  3. ├─dplyr::filter(., dplyr::across(dplyr::vars(!!!columns), ~dplyr::any_vars(!is.na(.))))
  4. ├─dplyr:::filter.data.frame(...)
  5. │ └─dplyr:::filter_rows(.data, ..., caller_env = caller_env())
  6. │   ├─base::withCallingHandlers(...)
  7. │   └─mask$eval_all_filter(dots, env_filter)
  8. └─dplyr::across(dplyr::vars(a, b), ~dplyr::any_vars(!is.na(.)))
  9.   └─dplyr:::across_setup(...)
 10.     └─tidyselect::eval_select(cols, data = across_cols)
 11.       └─tidyselect:::eval_select_impl(...)
 12.         ├─tidyselect:::with_subscript_errors(...)
 13.         │ ├─base::tryCatch(...)
 14.         │ │ └─base:::tryCatchList(expr, classes, parentenv, handlers)
 15.         │ │   └─base:::tryCatchOne(expr, names, parentenv, handlers[[1L]])
 16.         │ │     └─base:::doTryCatch(return(expr), name, parentenv, handler)
 17.         │ └─tidyselect:::instrument_base_errors(expr)
 18.         │   └─base::withCallingHandlers(...)
 19.         └─tidyselect:::vars_select_eval(...)
 20.           └─tidyselect:::walk_data_tree(expr, data_mask, context_mask)
 21.             └─tidyselect:::as_indices_sel_impl(...)
 22.               └─tidyselect:::as_indices_impl(x, vars, strict = strict)
 23.                 └─vctrs::vec_as_subscript(x, logical = "error")
Error: Must subset columns with a valid subscript vector.
x Subscript has the wrong type `quosures`.
ℹ It must be numeric or character.
Backtrace:
     █
  1. ├─global::function_new_dplyr(example_table, a, b)
  2. │ └─`%<>%`(...)
  3. ├─dplyr::filter(., dplyr::across(dplyr::vars(!!!columns), ~dplyr::any_vars(!is.na(.))))
  4. ├─dplyr:::filter.data.frame(...)
  5. │ └─dplyr:::filter_rows(.data, ..., caller_env = caller_env())
  6. │   ├─base::withCallingHandlers(...)
  7. │   └─mask$eval_all_filter(dots, env_filter)
  8. └─dplyr::across(dplyr::vars(a, b), ~dplyr::any_vars(!is.na(.)))
  9.   └─dplyr:::across_setup(...)
 10.     └─tidyselect::eval_select(cols, data = across_cols)
 11.       └─tidyselect:::eval_select_impl(...)
 12.         ├─tidyselect:::with_subscript_errors(...)
 13.         │ ├─base::tryCatch(...)
 14.         │ │ └─base:::tryCatchList(expr, classes, parentenv, handlers)
 15.         │ │   └─base:::tryCatchOne(expr, names, parentenv, handlers[[1L]])
 16.         │ │     └─base:::doTryCatch(return(expr), name, parentenv, handler)
 17.         │ └─tidyselect:::instrument_base_errors(expr)
 18.         │   └─base::withCallingHandlers(...)
 19.         └─tidyselect:::vars_select_eval(...)
 20.           └─tidyselect:::walk_data_tree(expr, data_mask, context_mask)
 21.             └─tidyselect:::as_indices_sel_impl(...)
 22.               └─tidyselect:::as_indices_impl(x, vars, strict = strict)
 23.                 └─vctrs::vec_as_subscript(x, logical = "error")
Error: Problem with `filter()` input `..1`.
ℹ Input `..1` is `dplyr::across(dplyr::vars(a, b), ~dplyr::any_vars(!is.na(.)))`.
x Must subset columns with a valid subscript vector.
x Subscript has the wrong type `quosures`.
ℹ It must be numeric or character.
Backtrace:
     █
  1. ├─global::function_new_dplyr(example_table, a, b)
  2. │ └─`%<>%`(...)
  3. ├─dplyr::filter(., dplyr::across(dplyr::vars(!!!columns), ~dplyr::any_vars(!is.na(.))))
  4. ├─dplyr:::filter.data.frame(...)
  5. │ └─dplyr:::filter_rows(.data, ..., caller_env = caller_env())
  6. │   ├─base::withCallingHandlers(...)
  7. │   └─mask$eval_all_filter(dots, env_filter)
  8. ├─dplyr::across(dplyr::vars(a, b), ~dplyr::any_vars(!is.na(.)))
  9. │ └─dplyr:::across_setup(...)
 10. │   └─tidyselect::eval_select(cols, data = across_cols)
 11. │     └─tidyselect:::eval_select_impl(...)
 12. │       └─tidyselect:::with_subscript_errors(...)
 13. │         └─base::tryCatch(...)
 14. │           └─base:::tryCatchList(expr, classes, parentenv, handlers)
 15. │             └─base:::tryCatchOne(expr, names, parentenv, handlers[[1L]])
 16. │               └─value[[3L]](cond)
 17. │                 └─rlang::cnd_signal(cnd)
 18. ├─rlang:::signal_abort(x)
 19. │ └─base::stop(fallback)
 20. └─(function (e) ...

I will have to spend more time with this but essentially it looks as if enquo is not meant to be used like this anymore "These tidy eval functions are no longer for normal usage, but are still exported from dplyr for backward compatibility."

Typical convention for ... I have seen has been something like this rather:

library(tidyverse)
library(rlang)
library(magrittr)

example_table <- tibble::tibble(a = c(1, 1, NA, 1), b = c(NA, NA, NA, 1), c = c(2, NA, NA, 2))

function_new_dplyr <- function(dataframe, ...) {
  
  columns <- list(...) %>% unlist()

  dataframe %<>%
    dplyr::select(!!!columns)
 
  return(dataframe)
}


function_new_dplyr(example_table, c("a", "b"))
#> # A tibble: 4 x 2
#>       a     b
#>   <dbl> <dbl>
#> 1     1    NA
#> 2     1    NA
#> 3    NA    NA
#> 4     1     1

Created on 2021-11-05 by the reprex package (v2.0.0)

Minimum you will have to do list(...).

I'll not claim that I've clear understanding of NSE (super far from it), and it's possible that I got the results just by luck. But still, here's my attempt:

library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union

example_table <- tibble(a = c(1, 1, NA, 1), b = c(NA, NA, NA, 1), c = c(2, NA, NA, 2))

# using new functions
step_1 <- function(dataframe, columns) {
  dataframe |>
    filter(if_any({{ columns }}, ~ !is.na(.)))
}

step_1(example_table, c(a, b))
#> # A tibble: 3 × 3
#>       a     b     c
#>   <dbl> <dbl> <dbl>
#> 1     1    NA     2
#> 2     1    NA    NA
#> 3     1     1     2

# using ellipsis
step_2 <- function(dataframe, ...) {
  dataframe |>
    filter(if_any(c(...), ~ !is.na(.)))
}

step_2(example_table, a, b)
#> # A tibble: 3 × 3
#>       a     b     c
#>   <dbl> <dbl> <dbl>
#> 1     1    NA     2
#> 2     1    NA    NA
#> 3     1     1     2

I'll highly appreciate if someone else provides a better solution with explanations :slight_smile:

3 Likes

Perfect, this is just what I needed!

I had tried out a configuration of this using if_any() like you did, but I was using the dplyr::vars() function at the .cols argument. It didn't even cross my mind to pass the dots into a vector at that position though!

If I may ask what this function is supposed to do:

Does it mean that it should find (check) NAs in a selected columns ?
Why if you selecting columns a and b, as a result that functions returns a, b, and c columns anyway ?

Hi @Andrzej , please see the reprex either in my original post or from Yarnabrina.

In short, this function will remove rows where NAs are present in all of the variables selected by a user. This could be 0, 1, 2, or ...N variables hence the need to use ...

Thank you @pcall,
This is smart:

Now I get it, that this function will remove rows where only NAs are present.

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.

If you have a query related to it or one of the replies, start a new topic and refer back with a link.