Search code examples
rnon-standard-evaluation

deparse(substitute()) returns function name normally, but function code when called inside for loop


I'm a bit surprised by R's behaviour in a very specific case. Let's say I define a function square that returns the square of its argument, like this:

square <- function(x) { return(x^2) }

I want to call this function within another function, and I also want to display its name when I do that. I can do that using deparse(substitute()). However, consider the following examples:

ds1 <- function(x) {
  print(deparse(substitute(x)))
}

ds1(square)
# [1] "square"

This is the expected output, so all is fine. However, if I pass the function wrapped in a list and process it using a for loop, the following happens:

ds2 <- function(x) {
  for (y in x) {
    print(deparse(substitute(y)))
  }
}

ds2(c(square))
# [1] "function (x) "   "{"               "    return(x^2)" "}"  

Can anybody explain to me why this occurs and how I could prevent it from happening?


Solution

  • As soon as you use x inside your function for the first time, it is evaluated, so it "stops being an unevaluated call" (the technical term in R for this is "promise") and "starts being its resulting values". To prevent this, you must capture x by substitute before you use it for the first time.

    (The reason for this behaviour lies in R's nature of being a language that does so-called "lazy evaluation", more on this and promises in "Advanced R"´)

    The result of substitute is an object of a type named "call" which you can query as if it was a list. So inside a function you can use

    x <- substitute(x)
    

    and then x[[1]] (the function name) and x[[2]] and following (the arguments of the function)

    So this works:

    ds2 <- function(x) {
        x <- substitute(x)
        # you can do `x[[1]]` but you can't use the expression object x in a
        # for loop. So you have to turn it into a list first
        for (y in as.list(x)[-1]) {
            print(deparse(y))
        }
    }
    ds2(c(square,sum))
    ## [1] "square"
    ## [1] "sum"
     
    

    Note that to complicate matters, substitute() only behaves like this when it is called inside a function and not outside on the top level (global environment). Outside of a function it turns its argument into a call object without replacing anything. So outside of a function, substitute() acts like quote(). That can be confusing if you try to tinker around with it:

    f <- function(x){
      x <- substitute(x)
      deparse(x)
    }
    
    # Replaces "x"
    f("hi")
    ## "\"hi\""
    
    # Does not replace x
    x <- "hi"
    deparse(substitute(x))
    ## "x"