Programming with a function that accepts bare arguments


#1

Hi folks,

I'm working on some code that wraps around a variety of functions, which I don't necessarily know ahead of time. Some of the functions accept bare variable names (with no options for, e.g., strings or formulas) and do eval like this:

f <- function(x, y, data) {
  mc <- match.call()
  mx <- mc[[match("x", names(mc))]]
  my <- mc[[match("y", names(mc))]]
  x <- eval(mx, data, enclos = sys.frame(sys.parent()))
  y <- eval(my, data, enclos = sys.frame(sys.parent()))
  c(x, y)
}

df <- data.frame(a = 1:5, b = 5:1)

f(a, b, df)
#>  [1] 1 2 3 4 5 5 4 3 2 1

But when I use a wrapper that passes arguments via ..., it fails:

wrapper <- function(data, .f = f, ...) {
  .f(data = data, ...)
}

wrapper(df, x = a, y = b)
#> Error in eval(mx, data, enclos = sys.frame(sys.parent())): object 'a' not found

Is there a general way to handle passing the arguments safely? A given function may or may not have that set up, and I can't change f() in such a way to do this more generally.

For my own knowledge, I'd also like to know why it fails. ... passes arguments without evaluating them, right? Despite the fact that it's failing in eval(), I have a feeling that that's misleading. Is it something about then needing to take the argument from the matched call and inserting it into eval() that evaluates it out of context? Or is it in fact something going on in eval()?

Thanks!
Malcolm


#2

Hi Malcolm,

It's possible that I am missing something big here, but this seems like a perfect application for tidy eval.

You might have a look at Hadley's presentation from rstudio::conf in January, as a way to get started.

Edwin Thoen has a great blog post on this, as well.

Hope this helps,

Ian


#3

Hi Ian,

I should have mentioned; I tried tidy eval and didn't get very far. I guess I haven't digested tidy eval well enough to do it correctly. The closest I've gotten is this madness:

tidyeval_wrapper <- function(data, .f = f, ...) {
  .args <- enquos(...)
  .f(data = data, eval_tidy(exprs(!!!.args)), data)
}

tidyeval_wrapper(df, x = a, y = b)
#> $x
#> a
#> 
#> $y
#> b
#> 
#> $a
#> [1] 1 2 3 4 5
#> 
#> $b
#> [1] 5 4 3 2 1

#4

Hello Malcolm,

If I understand your problem correctly, basically you want to emulate call f(data = df, x = a, y = b) with wrapper(df, .f = f, x = a, x = b). Here is one way to do it (which is pretty close to yours):

library(rlang)
tidyeval_wrapper_expr <- function(data, .f = f, ...) {
  .args <- enexprs(...)
  eval(expr(.f(data = data, !!! .args)))
}

tidyeval_wrapper_expr(df, x = a, y = b)
#>  [1] 1 2 3 4 5 5 4 3 2 1

tidyeval_wrapper_expr() does the following:

  • Converts ... into a list of unevaluated expressions: first element (named x) contains symbol a, second (named y) - b.
  • With expr(.f(data = data, !!! .args)) constructs expression .f(data = data, x = a, y = b).
  • Evaluates this expression.

Hope it helps.


#5

Bingo! Thanks, @echasnovski! I had thought enexprs() might be the better choice, but I hadn't realize the entire function needed to be an expression to properly construct it. This works beautifully for both the reprex and so far in my real life code. Thanks again!