Okay I think I've finally figured this out completely. Don't take this for gospel, but this is my understanding. Here is my updated solution, I'll explain.
foo <- function(x, args) {
args_call <- rlang::enexpr(args)
list_of_args <- rlang::lang_args(args_call)
mutate(x, !!! list_of_args)
}
From what I can tell, args is not technically a list yet, it's a promise that will create a list, but once you use lapply() (or any function that forces the evaluation of the promise) on that promise directly you force the expression cyl * 2 to attempt to be evaluated. Without being in the mutate call it won't have the correct environment to evaluate correctly.
What we really want is to just turn the user's call, list(cyl2=cyl*2, y=mpg/2) , into a real list of named arguments, but without evaluating the arguments. This is exactly what lang_args does to start with. It sees the call, extracts the arguments without evaluating them, and returns them to you in a real named list. That's all we need to pass to !!!.
I'm also not sure we even need enquo() here, it seems that enexpr() works just the same because we don't need the environment the call was created in. That's the reasoning for using enexpr() above.
So to summarise:
- Capture the user's call with
enexpr().
- Extract the named arguments with
lang_args(), turning them into a named list of arguments that can be used with !!!.
The hilarious thing is that since we never actually evaluate the list function, it can really be anything.
# This works with `c()`
foo(mtcars %>% group_by(gear), args = c(cyl2=cyl*2, y = mean(carb)))
# This works with `not_a_function()`
foo(mtcars, args = not_a_function(cyl2=cyl*2, y = mean(carb)))