3

I created a function to convert a function name to string. Version 1 func_to_string1 works well, but version 2 func_to_string2 doesn't work.

func_to_string1 <- function(fun){
    print(rlang::as_string(rlang::enexpr(fun)))
}

func_to_string2 <- function(fun){
    is.function(fun)
    print(rlang::as_string(rlang::enexpr(fun)))
}

func_to_string1 works:

> func_to_string1(sum)
[1] "sum"

func_to_string2 doesn't work.

> func_to_string2(sum)
 Error: Can't convert a primitive function to a string
Call `rlang::last_error()` to see a backtrace 

My guess is that by calling the fun before converting it to a string, it gets evaluated inside function and hence throw the error message. But why does this happen since I didn't do any assignments?

My questions are why does it happen and is there a better way to convert function name to string?

Any help is appreciated, thanks!

yusuzech
  • 5,896
  • 1
  • 18
  • 33

3 Answers3

4

Maybe use this instead?

func_to_string2 <- function(fun){
  is.function(fun)
  deparse(substitute(fun)) 
  #print(rlang::as_string(rlang::enexpr(fun)))
}
> func_to_string2(sum)
[1] "sum"
cory
  • 6,529
  • 3
  • 21
  • 41
4

This isn't a complete answer, but I don't think it fits in a comment.

R has a mechanism called pass-by-promise, whereby a function's formal arguments are lazy objects (promises) that only get evaluated when they are used. Even if you didn't perform any assignment, the call to is.function uses the argument, so the promise is "replaced" by the result of evaluating it.

Nevertheless, in my opinion, this seems like an inconsistency in rlang*, especially given cory's answer, which implies that R can still find the promise object even after a given parameter has been used; the mechanism to do so might not be part of R's public API though.

*EDIT: see coments.

Regardless, you could treat enexpr/enquo/ensym like base::missing, in the sense that you should only use them with parameters you haven't used at all in the function's body.

Alexis
  • 4,950
  • 1
  • 18
  • 37
  • This is not an inconsistency in rlang. Once a promise has been forced, it no longer has an environment, which means we can't create a quosure from it. We decided to make `enexpr()` consistent with `enquo()`. – Lionel Henry Aug 15 '19 at 05:53
  • If you don't need quasiquotation or a quosure, then using `substitute()` makes sense. – Lionel Henry Aug 15 '19 at 05:54
  • @LionelHenry I see, I guess that makes sense, although documentation could be more specific, since it does say `enexpr` corresponds to `substitute`, yet `enexpr` can return non-expressions if a given promise has already been evaluated. Also, in a quick test, `enquo` does create a quosure, though with the empty environment. – Alexis Aug 15 '19 at 08:10
  • In some cases `substitute()` might also return non-expressions because the bytecode compiler unwraps constants from promises. – Lionel Henry Aug 16 '19 at 14:01
1

This question brings up an interesting point on lazy evaluations.

R arguments are lazily evaluated, meaning the arguments are not evaluated until its required.

This is best understood in the Advanced R book which has the following example,

f <- function(x) {
  10
}
f(stop("This is an error!"))

the result is 10, which is surprising because x is never called and hence never evaluated. We can force x to be evaluated by using force()

f <- function(x) {
  force(x)
  10
}
f(stop("This is an error!"))

This behaves as expected. In fact we dont even need force() (Although it is good to be explicit).

f <- function(x) {
  x
  10
}
f(stop("This is an error!"))

This what is happening with your call here. The function sum which is a symbol initially is being evaluated with no arguments when is.function() is being called. In fact, even this will fail.

func_to_string2 <- function(fun){
  fun
  print(rlang::as_string(rlang::ensym(fun)))
}

Overall, I think its best to use enexpr() at the very beginning of the function.

Source:

http://adv-r.had.co.nz/Functions.html

Sada93
  • 2,785
  • 1
  • 10
  • 21