Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

r

dplyr

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?

like image 278
Stormwalker Avatar asked Jan 29 '18 15:01

Stormwalker


2 Answers

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)
like image 191
MrFlick Avatar answered Oct 22 '22 05:10

MrFlick


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.

like image 4
Lief Esbenshade Avatar answered Oct 22 '22 06:10

Lief Esbenshade