Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Defer expression evaluation without using `quote`

I have created the following function/example as a generic way to display variable labels in tables and so forth:

#' Function to prettify the output of another function using a `var.labels` attribute
#' This is particularly useful in combination with read.dta et al.
#' @param dat A data.frame with attr `var.labels` giving descriptions of variables
#' @param expr An expression to evaluate with pretty var.labels
#' @return The result of the expression, with variable names replaced with their labels
#' @examples
#' testDF <- data.frame( a=seq(10),b=runif(10),c=rnorm(10) )
#' attr(testDF,"var.labels") <- c("Identifier","Important Data","Lies, Damn Lies, Statistics")
#' prettify( testDF, quote(str(dat)) )
prettify <- function( dat, expr ) {
  labels <- attr(dat,"var.labels")
  for(i in seq(ncol(dat))) colnames(dat)[i] <- labels[i]
  attr(dat,"var.labels") <- NULL
  eval( expr )
}

I'd rather the user not have to quote the expression being passed in, however.

replicate does just that, using this code:

eval.parent(substitute(function(...) expr))

Yet I don't understand how it works, and as is typical for attempts to replicate without comprehension, my attempts to simply copy this code or tinker with it have all failed.

How do I write a function with an unevaluated expression as input, without requiring the user to quote their expression? I assume the answer will rely heavily on lazy evaluation.

like image 579
Ari B. Friedman Avatar asked Feb 23 '13 10:02

Ari B. Friedman


1 Answers

Answer with eval and substitute

I think to do this in this case you just need eval(substitute(expr)). expr is a promise, and we can either get the value of the promise by using expr directly, or the content of the promise, by using substitute. See http://cran.r-project.org/doc/manuals/R-lang.html#Promise-objects for details. The content of the promise is a call, so we just eval that to get the new result.

prettify <- function( dat, expr ) {
  labels <- attr(dat,"var.labels")
  for(i in seq(ncol(dat))) colnames(dat)[i] <- labels[i]
  attr(dat,"var.labels") <- NULL
  eval(substitute(expr))
}

> prettify( testDF, str(dat))
'data.frame':   10 obs. of  3 variables:
 $ Identifier                 : int  1 2 3 4 5 6 7 8 9 10
 $ Important Data             : num  0.336 0.9479 0.1379 0.94 0.0484 ...
 $ Lies, Damn Lies, Statistics: num  1.398 0.654 0.268 -0.397 -0.41 ...

In a suggested edit, @user2103369 suggests that replicate is different because it uses sapply to get multiple evaluations, so it needs a function rather than a call.

Different behavior when default argument

Interestingly, the promise acts differently depending on if the argument is the default argument or added by the user; see below. I think SoDA addresses this but I don't have it handy. This function prints the value of the promise, evaluates it with eval, and then evaluates it directly.

foo <- function(a, b=a+1) {
  print(substitute(b))
  print(eval(substitute(b)))
  b
}

Evaluating it directly results in an error when the user supplies the value.

> foo(a=2, b=a+1)
a + 1
[1] 3
Error in foo(a = 2, b = a + 1) : object 'a' not found

But the default value works.

> foo(a=2)
a + 1
[1] 3
[1] 3

In a suggested edit, @user2103369 says that the default argument is evaluated inside the function, while an explicit argument is evaluated in the calling frame. So in this case, the user supplied value fails because a is not visible in the calling frame.

An alternate method using a function

However, to me (though the OP disagrees; I'm leaving this part for future readers of this answer), this feels like a case where it's more natural to use a function as the second parameter, like this; for one, this means the user doesn't have to know that it's called dat within the function.

prettify <- function( dat, FUN ) {
  f <- match.fun(FUN)
  labels <- attr(dat,"var.labels")
  for(i in seq(ncol(dat))) colnames(dat)[i] <- labels[i]
  attr(dat,"var.labels") <- NULL
  f(dat)
}

Then it can be called with an anonymous function, which is exactly what you're looking for, I think, except that the user has to type function(x) as well.

> prettify( testDF, function(x) str(x) )
'data.frame':   10 obs. of  3 variables:
 $ Identifier                 : int  1 2 3 4 5 6 7 8 9 10
 $ Important Data             : num  0.296 0.707 0.883 0.821 0.724 ...
 $ Lies, Damn Lies, Statistics: num  -1.1506 0.4846 -1.824 -0.397 0.0898 ...

Or in simple cases, as in your example, with just the name of the function.

> prettify( testDF, str)
'data.frame':   10 obs. of  3 variables:
 $ Identifier                 : int  1 2 3 4 5 6 7 8 9 10
 $ Important Data             : num  0.296 0.707 0.883 0.821 0.724 ...
 $ Lies, Damn Lies, Statistics: num  -1.1506 0.4846 -1.824 -0.397 0.0898 ...
like image 89
Aaron left Stack Overflow Avatar answered Oct 17 '22 07:10

Aaron left Stack Overflow