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?
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)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With