Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pass a single argument as dots in tidyeval

Tags:

r

dplyr

rlang

I am trying to wrap dplyr::filter within a function where when there is more than one filter condition, then they are passed as a vector or list. See this minimal example:

filter_wrap <- function(x, filter_args) {
  filter_args_enquos <- rlang::enquos(filter_args)
  dplyr::filter(x, !!!filter_args_enquos)
}

When there is a single condition I can make it work:

data(iris)
message("Single condition works:")
expected <- dplyr::filter(iris, Sepal.Length > 5)
obtained <- filter_wrap(iris, filter_args = Sepal.Length > 5)
stopifnot(identical(expected, obtained))

When I try to pass more than one condition I get a problem. I was expecting that the !!! operator in the dplyr::filter call would splice my arguments but given the error message I guess I am understanding it wrong.

message("Multiple conditions fail:")
expected <- dplyr::filter(iris, Sepal.Length > 5, Petal.Length > 5)
obtained <- filter_wrap(iris, c(Sepal.Length > 5, Petal.Length > 5))
# Error in filter_impl(.data, quo) : Result must have length 150, not 300
# Called from: filter_impl(.data, quo)
stopifnot(identical(expected, obtained))

Using a list does change the error message:

obtained <- filter_wrap(iris, list(Sepal.Length > 5, Petal.Length > 5))
# Error in filter_impl(.data, quo) : 
#  Argument 2 filter condition does not evaluate to a logical vector
# Called from: filter_impl(.data, quo)

I don't want to use ... as my function will have other arguments and I may want to use dots for something else.

How can I expand my filter_args argument when passing it to dplyr::filter?

like image 330
zeehio Avatar asked Nov 13 '18 12:11

zeehio


2 Answers

Basically your problem is that when you call enquos() on the single parameter, you are also quoting the list() call (which is a single call). So basically you are creating

filter_args_enquos <- quo(list(Sepal.Length > 5, Petal.Length > 5))

and when you call

dplyr::filter(iris, !!!filter_args_enquos)

that's the same as

dplyr::filter(iris, list(Sepal.Length > 5, Petal.Length > 5))

which isn't valid dplyr syntax. The !!! needs to work on a proper list-like object, not an un-evaluated call to a list, note that this would work

filter_args_enquos <- list(quo(Sepal.Length > 5), quo(Petal.Length > 5))
dplyr::filter(iris, !!!filter_args_enquos)

because here we are actually evaluating the list, and only quoting the things inside the list. This is basically the type of object created by enquos when using ...

filter_wrap <- function(x, ...) {
  filter_args_enquos <- rlang::enquos(...)
  dplyr::filter(x, !!!filter_args_enquos)
}
filter_wrap(iris, Sepal.Length > 5, Petal.Length > 5)

The enquos() function expects multiple parameters, not just a single list. That's why it's meant to be used with ... because that will expand to multiple parameters. If you want to possibly pass in a list, you can write a helper function that can look for this special case and expand the quosure properly. For example

expand_list_quos <- function(x) {
  expr <- rlang::quo_get_expr(x)
  if (expr[[1]]==as.name("list")) {
    expr[[1]] <- as.name("quos")
    return(rlang::eval_tidy(expr, env = rlang::quo_get_env(x)))
  } else {
    return(x)
  }
}

Then you can use it with

filter_wrap <- function(x, filter_args) {
  filter_args <- expand_list_quos(rlang::enquo(filter_args))
  dplyr::filter(x, !!!filter_args)
}

and both of these will work

filter_wrap(iris, Petal.Length > 5)
filter_wrap(iris, list(Sepal.Length > 5, Petal.Length > 5))

but this is not really the way this enquo stuff is "meant" to be used. The ... method is much more idiomatic. Or calling quos() explicitly if you need more control

filter_wrap <- function(x, filter_args) {
  dplyr::filter(x, !!!filter_args)
}
filter_wrap(iris, quo(Petal.Length > 5))
filter_wrap(iris, quos(Sepal.Length > 5, Petal.Length > 5))
like image 109
MrFlick Avatar answered Nov 08 '22 05:11

MrFlick


I hope I understood it right, here is a quick'n'dirty solution: The problem is / was that by combining your logic queries combined by c which results in a vector as long as x * n of comparisons.

filter_wrap <- function(x, filter_args) {
     filter_args_enquos <- rlang::enquos(filter_args)
     LogVec <- rowwise(x) %>% mutate(LogVec = all(!!!filter_args_enquos)) %>%             
     pull(LogVec)
     dplyr::filter(x, LogVec)
}

expected <- dplyr::filter(iris, Sepal.Length > 5, Petal.Length > 5)
obtained <- filter_wrap(iris, c(Sepal.Length > 5, Petal.Length > 5))    

stopifnot(identical(expected, obtained))
like image 26
floe Avatar answered Nov 08 '22 04:11

floe