18
votes

Further delving into the mysteries of R evaluation...This is closely related to my previous question ( How to write an R function that evaluates an expression within a data-frame ). Let's say I want to write a function topfn that takes a data-frame and an expression involving column-names of that data-frame. I want to pass both these arguments on to another function fn that actually evaluates the expression within the "environment" of the data-frame. And I want both fn and topfn to work correctly when passed a data-frame and an expression

My first attempt, as suggested in the answer to the above question, is to define:

 fn <- function(dfr, expr) {
   mf <- match.call()
   eval( mf$expr, envir = dfr )
 }

And define topfn like this:

topfn <- function(df, ex) {
  mf <- match.call()
  fn(df, mf$ex) 
}

Now if I have a data-frame

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

the inner function fn works fine:

> fn(df,a)
[1] 1 2 3 4 5

But the topfn does not work:

> topfn(df,a)
mf$ex

To fix this I first check the class of topfn(df,a),

> class(topfn(df,a))
[1] "call"

This gives me an idea for an ugly hack to re-define fn as follows:

fn <- function(dfr, expr) {
  mf <- match.call()
  res <- eval(mf$expr, envir = dfr)  
  if(class(res) == 'call')
    eval(expr, envir = dfr) else
  res
}

And now both functions work:

> fn(df,a)
[1] 1 2 3 4 5
> topfn(df,a)
[1] 1 2 3 4 5

As I said, this looks like an ugly hack. Is there a better way (or more standard idiom) to get these working? I've consulted Lumley's curiously-named Standard NonStandard Evaluation Rules document http://developer.r-project.org/nonstandard-eval.pdf but wasn't particularly enlightened after reading it. Also helpful would be any pointers to source-code of functions I can look at for examples.

2
There is a great wiki on the subject of R evaluation by Hadley Wickham here: github.com/hadley/devtools/wiki/Evaluation - Prasad Chalasani
And as I suggest there you want carefully distinguish between functions that are used interactively, and functions that are called from other functions. It is very difficult to call any function that uses substitute and other tricks. In other words, there is no clean way for both fn and topfn to work with the same types of inputs - and this a GOOD thing. - hadley
@hadley I see your point: functions designed for interactive use can use substitute/other tricks to make it easy to type them at the console (i.e. no quotes, etc). And functions that are only designed to be called by other functions should not use such tricks because they may break unpredictably. In particular, the solution provided by @Richie at the end of his answer, even though it works in my particular scenario above -- it may break (especially fn ) when used in some other scenario -- I think that's what you're saying, right? - Prasad Chalasani
I would be able to give a more concrete advice if you could give more details about what you're trying to achieve - it's too hard to help in the abstract. - hadley
Two options: go the plyr/ggplot route and use a special function quote. . from plyr would probably suit your needs. Otherwise, write a programmer and interactive version of each function: the interactive version captures the expressions and then calls on. Either way will save you a lot of pain in the long run. - hadley

2 Answers

15
votes

This is most easily avoided by passing strings into topfn instead of expressions.

topfn <- function(df, ex_txt) 
{
  fn(df, ex_txt) 
}

fn <- function(dfr, expr_txt) 
{        
   eval(parse(text = expr_txt), dfr) 
}

df <- data.frame(a = 1:5, b = 1:5 )
fn(df, "a")                              
fn(df, "2 * a + b")
topfn(df, "a")             
topfn(df, "2 * a + b")

EDIT:

You could let the user pass expressions in, but use strings underneath for your convenience.

Change topfn to

topfn <- function(df, ex) 
{
  ex_txt <- deparse(substitute(ex))
  fn(df, ex_txt) 
}
topfn(df, a)             
topfn(df, 2 * a + b)

ANOTHER EDIT:

This seems to work:

topfn <- function(df, ex) 
{
  eval(substitute(fn(df, ex)))
}

fn <- function(dfr, expr) 
{        
   eval(substitute(expr), dfr) 
}
fn(df, a)                              
fn(df, 2 * a + b)
topfn(df, a)             
topfn(df, 2 * a + b)
1
votes

You can use three dots to gather arguments and pass them to another function, is that what you mean?

ftop=function(...) f(...)
f=function(a,b) a[b]

a=data.frame(b=10)

ftop(a,"b")

f(a,"b")