Stormwalker
Stormwalker

Reputation: 361

Testing a function that uses enquo() for a NULL parameter

I have a function which creates dataframe, but changes names in the process. I am trying to handle empty column names with dplyr quosures. My test suite looks like this:

dataframe <- data_frame(
  a = 1:5,
  b = 6:10
)

my_fun <- function(df, col_name, new_var_name = NULL) {
  target <- enquo(col_name)

  c <- df %>% pull(!!target) * 3 # here may be more complex calculations

  # handling NULL name
  if (is.null(new_var_name)) {
    new_name <- quo(default_name)
  } else{
    new_name <- enquo(new_name)
  }

  data_frame(
    abc = df %>% pull(!!target),
    !!quo_name(new_name) := c
  )
}

And if I call my function like this:

my_fun(dataframe, a)

I get default name as intended:

# A tibble: 5 x 2
    abc default_name
  <int>        <dbl>
1     1            3
2     2            6
3     3            9
4     4           12
5     5           15

And if I'm trying to pass name I get error:

my_fun(dataframe, a, NEW_NAME)
Error in my_fun(dataframe, a, NEW_NAME) : object 'NEW_NAME' not found

Where am I wrong?

Upvotes: 7

Views: 1340

Answers (2)

Lief Esbenshade
Lief Esbenshade

Reputation: 833

The approach outlined by MrFlick does not work with nested function calls. We can use rlang::quo_is_null instead.

From the documentation on rlang::quo_is_null : "When missing arguments are captured as quosures, either through enquo() or quos(), they are returned as an empty quosure". So when we nest function calls with empty quosures, the call to missing in the inner function ends up checking whether an empty quosure is NULL, and always returns FALSE since it is the contents of the quosure that is NULL and not the quosure itself.

I put together the following verbose functions to show what is occurring:

library(dplyr)
library(rlang)

f1 <- function(var = NULL) {
    
    print(paste("substitute shows:", paste(substitute(var), collapse = " ")))
    print(paste("missing returns:", missing(var)))
    
    enquo_var <- enquo(var)
    print(paste("after enquo:", quo_get_expr(enquo_var)))
    print(paste("quo_is_null returns:", rlang::quo_is_null(enquo_var)))
    
    rlang::quo_is_null(enquo_var)
}

f2 <- function(var = NULL) {
    
    f1({{var}})
}

f1(Sepal.Length) 
f1() 

f2(Sepal.Length) 
f2() # this is where `missing` fails. 

NB: I welcome corrections or additions to this explanation. Many thanks to mrflick, Lionel Henry, and Hugh. See here for a related question.

Upvotes: 4

MrFlick
MrFlick

Reputation: 206536

This problem doesn't really have to do with quo and enquo returning different things, it's really about evaluating objects before you really want to. If you were to use the browser() to step through your function, you'd see the error occurs at the if (is.null(new_var_name)) statement.

When you do is.null(new_var_name), you are evaluating the variable passed as new_var_name so it's too late to enquo it. That's because is.null needs to look at the value of the variable rather than just the variable name itself.

A function that does not evaluate the parameter passed to the function but checks to see if it is there is missing().

my_fun <- function(df, col_name, new_var_name=NULL) {
  target <- enquo(col_name)

  c <- df %>% pull(!!target) * 3 # here may be more complex calculations

  # handling NULL name
  if (missing(new_var_name)) {
    new_name <- "default_name"
  } else{
    new_name <- quo_name(enquo(new_var_name))
  }

  data_frame(
    abc = df %>% pull(!!target),
    !!new_name := c
  )
}

Then you can run both of these

my_fun(dataframe, a)
my_fun(dataframe, a, NEW_NAME)

Upvotes: 13

Related Questions