A difference between tidyeval and substitute() when the promise is already evaluated

I created a function that returns expression as text except when the input is NULL and wondered why the following two functions using tidyeval and base-NSE behave differently:

f_rlang <- function(x) {
  if (is.null(x)) return("")
  rlang::expr_text(rlang::enexpr(x))
}

f_base <- function(x) {
  if (is.null(x)) return("")
  deparse(substitute(x))
}

symbol <- "content"

f_rlang(symbol)
#> [1] "\"content\""
f_base(symbol)
#> [1] "symbol"

Created on 2018-10-02 by the reprex package (v0.2.1)

After some investigation, in tidyeval I found I have to capture x before evaluating it. So, the correct version would be:

f_rlang2 <- function(x) {
  x_expr <- rlang::enexpr(x)
  if (is.null(x)) return("")
  rlang::expr_text(x_expr)
}

symbol <- "content"

f_rlang2(symbol)
#> [1] "symbol"

Created on 2018-10-02 by the reprex package (v0.2.1)

But, is this what is supposed to be? If this is quo(), it may be reasonable that it matters whether the promise (right?) is evaluated or not. But, this is expression, which is separated from the environments and doesn't know when and where to get evaluated.

Since I'm not familiar with tidyeval's concepts, I'm not really sure if I should file a issue for this on rlang's repo. Does anyone know are there any nice explanation for this difference between tidyeval and base-NSE?

1 Like

From the rlang docs:

In terms of base functions, enexpr(arg) corresponds to base::substitute(arg) (though that function has complex semantics) and expr() is like quote() (and bquote() if we consider unquotation syntax). The plural variant exprs() is equivalent to base::alist() . Finally there is no function in base R that is equivalent to enexprs() but you can reproduce its behaviour with eval(substitute(alist(...))) .

So I think you can just use:

library(rlang, warn.conflicts = FALSE)
f_rlang3 <- function(x) {
  if (is.null(x)) return("")
  rlang::enexpr(x)
}

symbol <- "content"
f_rlang3(symbol)
#> [1] "content"

Created on 2018-10-02 by the reprex package (v0.2.1.9000)

Thanks, but it's not what I wanted... I expect this:

f_base(symbol)
#> [1] "symbol"

And, to be clear, the core of my question is "Why does it matter on enexpr() whether the argument is already evaluated or not?"

A forced promise can no longer be captured correctly because it no longer carries an environment. enexpr() does not need the environment but we chose to make all capture functions consistent with enquo().

For the use-case of labelling (which seems relevant here), we are thinking about new capture operators: https://github.com/r-lib/rlang/issues/303.

In the meantime I would just use substitute().

1 Like

Ah! I've seen that issue, but didn't understand in what situation it would be needed at that time... Now I got it, thanks so much!!!

(But, I still feel a little bad that enexpr() returns a value rather than expression, though it's too late to change the name)

Right but even without this, expr() can return constants: expr(!!letters). In fact tidy eval is implemented in such a way that capturing a forced argument or an unquoted value is strictly equivalent. It is thus good practice to always keep in mind unquoted values when designing your quoting function. This shouldn't be a problem if you're taking an evaluation approach (rather than a parsing or labelling approach).

2 Likes

OK, now I feel it's fair, thanks for the explanation. What I wanted to do was labelling, so I'll wait for enlabel() and stick with substitute() for labelling purpose until the time comes :slight_smile: