Non-standard evaluation question about RHS

I am wrapping up the development of my first CRAN submission and ran into an issue with our use of non-standard evaluation.

The following is a simplified reprex that reproduces our issue. The function a() works just fine if sourceVar is unquoted in the initial call. However, it throws an error if sourceVar is quoted.

suppressMessages(library(dplyr))
library(rlang)

testData <- data.frame(x = c(1, 2, 3, 4, 5))

a <- function(.data, sourceVar, cleanVar) {
  # quote input
  cleanVar <- quo_name(enquo(cleanVar))
  sourceVar <- enquo(sourceVar)

  # create new variable
  mutate(.data, !!cleanVar := (!!sourceVar)*2)
}

a(testData, sourceVar = x, cleanVar = y)
#>   x  y
#> 1 1  2
#> 2 2  4
#> 3 3  6
#> 4 4  8
#> 5 5 10
a(testData, sourceVar = "x", cleanVar = y)
#> Error in mutate_impl(.data, dots): Evaluation error: non-numeric argument to binary operator.

What is curious to me is not just that this fails, but it only seems to matter on the right side of our mutate call. Regardless if we quote or unquote the input for cleanVar, it successfully evaluates a():

suppressMessages(library(dplyr))
library(rlang)

testData <- data.frame(x = c(1, 2, 3, 4, 5))

b <- function(.data, sourceVar, cleanVar) {
  # quote input
  cleanVar <- quo_name(enquo(cleanVar))
  sourceVar <- enquo(sourceVar)
  
  # create new variable
  mutate(.data, !!cleanVar := (!!sourceVar)*2)
}

b(testData, sourceVar = x, cleanVar = y)
#>   x  y
#> 1 1  2
#> 2 2  4
#> 3 3  6
#> 4 4  8
#> 5 5 10
b(testData, sourceVar = x, cleanVar = "y")
#>   x  y
#> 1 1  2
#> 2 2  4
#> 3 3  6
#> 4 4  8
#> 5 5 10

Any help schooling me on where I am going wrong here, or how we can flexibly accommodate quoted and unquoted inputs on the righthand side our of mutate calls, would be much appreciated!

Just to clarify, the reason that it "works" on the left-hand side is that mutate will accept characters or bare names on the left of the assignment operator:

suppressMessages(library(dplyr))

testData <- data.frame(x = c(1, 2, 3, 4, 5))

mutate(testData, "y" = x * 2)
#>   x  y
#> 1 1  2
#> 2 2  4
#> 3 3  6
#> 4 4  8
#> 5 5 10

On the right side, it becomes equivalent to mutate(testData, y = "x" * 2), which produces the error that you see.

If you want to accommodate quoted and unquoted inputs, I believe you have to detect whether or not the input is a character vs. a bare name. There is some discussion of that at this SO link:

However, I suspect that there is a better way to do it in this case, so I won't try to translate that method into your use. Hopefully someone else knows the "right" answer!

3 Likes

Thank you so much! I don't actually like the solution in the SO example, which called different versions of ggplot() based on whether it was string or bare.

In my case, where the actual dplyr call is quite a bit longer than in the reprex, I think defining a variable called source based on whether the sourceVar is string or not is a more efficient approach:

suppressMessages(library(dplyr))
library(rlang)

testData <- data.frame(x = c(1, 2, 3, 4, 5))

c <- function(.data, sourceVar, cleanVar) {
  lst <- as.list(match.call())
  
  # quote input
  cleanVar <- quo_name(enquo(cleanVar))
  
  if (!is.character(lst$sourceVar)) {
    source <- enquo(sourceVar)
  } else if (is.character(lst$sourceVar)) {
    source <- quo(!! sym(sourceVar))
  }
  
  # create new variable
  mutate(.data, !!cleanVar := (!!source)*2)
}

c(testData, sourceVar = x, cleanVar = y)
#>   x  y
#> 1 1  2
#> 2 2  4
#> 3 3  6
#> 4 4  8
#> 5 5 10
c(testData, sourceVar = "x", cleanVar = y)
#>   x  y
#> 1 1  2
#> 2 2  4
#> 3 3  6
#> 4 4  8
#> 5 5 10

The quo(!! sym(sourceVar) trick is from this issue that I found before I posted here but hadn't been able to work into my code in a way that effectively solved the problem. Now, with the as.list(match.call()), it plays a key role!

Thanks again, Nick! Looking forward to getting this package out the door...

2 Likes