What is the difference between using the rlang embrace operator `{{}}` and using `!!enquo()`?

81 views Asked by At

While writing a toy version of the + operator that only adds the furthest left and right hand sides of the sum, I came across this situation where {{}} functions as I expected and !!rlang::enquo() signals an error.

On the Embrace Operator reference page it says that under the hood "{{ combines enquo() and !! in one step". So in this case, why don't {{}} and !!rlang::enquo() function the same way?

library(rlang)

# Outside-only sum using `{{}}`
`%+%` <- function(lhs, rhs) {
  
  # Quote the `lhs` to inspect
  lhs_expr <- rlang::quo_get_expr(rlang::enquo(lhs))
  lhs_call <- lhs_expr[[1]]
  
  # Base Case
  if (lhs_call != as_name("%+%")) {
    lhs <- eval(lhs) # Evaluate `lhs` as it may still be a call
    return(lhs + rhs)
  }
  
  # Repeat using the left-hand-side of the next `%+%` as the new `lhs`
  lhs_first_arg <- lhs_expr[[2]]
  {{ lhs_first_arg }} %+% rhs
}

1 %+% 1
#> [1] 2
1 %+% 2 %+% 3 %+% 4 # 5
#> [1] 5
mean(c(1, 2, 3)) %+% "A" %+% "B" %+% -10
#> [1] -8

# Outside-only sum using `!!enquo()`
`%+%` <- function(lhs, rhs) {
  
  # Quote the `lhs` to inspect
  lhs_expr <- rlang::quo_get_expr(rlang::enquo(lhs))
  lhs_call <- lhs_expr[[1]]
  
  # Base Case
  if (lhs_call != as_name("%+%")) {
    lhs <- eval(lhs) # Evaluate `lhs` as it may still be a call
    return(lhs + rhs)
  }
  
  # Repeat using the left-hand-side of the next `%+%` as the new `lhs`
  lhs_first_arg <- lhs_expr[[2]]
  (!!enquo(lhs_first_arg)) %+% rhs
}

1 %+% 1
#> [1] 2
try(1 %+% 2 %+% 3 %+% 4)
#> Error in eval(lhs) : 
#>   Quosures can only be unquoted within a quasiquotation context. 
#> 
#> # Bad: list(!!myquosure)
#> 
#> # Good: dplyr::mutate(data, !!myquosure)

Created on 2023-11-15 with reprex v2.0.2

P.S. Another version which doesn't use enquo() in the recursive case also works fine - so does {{}} pass through bare expressions and !!enquo() not?

library(rlang)

# Outside-only sum, not quoting `lhs` in the recursive case
`%+%` <- function(lhs, rhs, fn = rlang::caller_fn()) {
  
  # Quote the `lhs` to inspect
  if (!identical(fn, `%+%`)) {
    lhs_expr <- rlang::quo_get_expr(rlang::enquo(lhs))
  } else {
    lhs_expr <- lhs
  }
  lhs_call <- lhs_expr[[1]]
  
  # Base Case
  if (lhs_call != as_name("%+%")) {
    lhs <- eval(lhs) # Evaluate `lhs` as it may still be a call
    return(lhs + rhs)
  }
  
  # Repeat using the left-hand-side of the next `%+%` as the new `lhs`
  lhs_first_arg <- lhs_expr[[2]]
  lhs_first_arg %+% rhs
}

1 %+% 1
#> [1] 2
1 %+% 2 %+% 3 %+% 4 # 5
#> [1] 5
mean(c(1, 2, 3)) %+% "A" %+% "B" %+% -10
#> [1] -8

Created on 2023-11-15 with reprex v2.0.2

0

There are 0 answers