Why doesn't forwarding work in this case?

Viewed 69

I had some trouble to forward '...' in R.
I've found a solution but I still don't understand why my original code was incorrect. Maybe someone can explain it to me ?

I have prepared a reprex:

# modify a string with glue.
# z is an optional argument
fun_A <- function(my_string, ..., z = NULL){
  print(paste0("z value: ", as.character(z))) # do something with z here
  print(list(...)) # to test: print ... to check that all variables are still there

  my_string <- glue::glue(my_string, ...)
  my_string
}

# when fun_A() is used inside fun_B, 'z' must be filled with the same value in fun_A() and fun_B().
fun_B <- function(z, my_string, ...){
  my_string_mod <- rlang::enquo(my_string) %>%
    rlang::call_modify(z = z) %>%
    rlang::eval_tidy()
  my_string_mod
  # then other stuff, useless for the reprex
}

# calls fun_B() but for a specific string, now the argument of my_string are explicit.
fun_C_ok <- function(x, y){
  fun_B(z = "i am z",
        my_string = fun_A(my_string = "replace {x} and {y}.",
                          x = !!x, y = !!y)
  )

Everything works as I want:

In fun_A(), 'z' is missing as expected. 'x' and 'y' are changed as expected

> fun_A(my_string = "replace {x} and {y}.", x = "this", y = "that")
[1] "z value: "
$x
[1] "this"

$y
[1] "that"

replace this and that.

In fun_B(), 'z' is modified in fun_A() as expected. 'x' and 'y' are changed as expected

> fun_B(z = "i am z", my_string = fun_A(my_string = "replace {x} and {y}.", x = "this", y = "that"))
[1] "z value: i am z"
$x
[1] "this"

$y
[1] "that"

replace this and that.

In fun_C_ok(), 'z' is modified in fun_A() as expected. 'x' and 'y' are changed as expected

> fun_C_ok(x = "this", y = "that")
[1] "z value: i am z"
$x
[1] "this"

$y
[1] "that"

replace this and that.

At first, i wrote the last function like this (without the bang-bang operator (!!))

fun_C_notok <- function(x, y){
  fun_B(z = "i am z",
        my_string = fun_A(my_string = "replace {x} and {y}.",
                          x = x, y = y)
  )
}

It wasn't working, 'x' was not found by glue inside fun_A() but I don't understand why because 'x' and 'y' exist in list(...)

> fun_C_notok(x = "this", y = "that")

[1] "z value: i am z"
$x
[1] "this"

$y
[1] "that"

 Error in eval(parse(text = text, keep.source = FALSE), envir) : 
  object 'x' not found 

11. eval(parse(text = text, keep.source = FALSE), envir) 
10. eval(parse(text = text, keep.source = FALSE), envir) 
9. .transformer(expr, env) 
8. (function (expr) 
{
    eval_func <- .transformer(expr, env)
    tryCatch(as.character(eval_func), error = function(e) { ... 
7. glue_data(.x = NULL, ..., .sep = .sep, .envir = .envir, .open = .open, 
    .close = .close, .na = .na, .transformer = .transformer, 
    .trim = .trim) 
6. glue::glue(my_string, ...) 
5. fun_A(my_string = "replace {x} and {y}.", x = x, y = y, z = "i am z") 
4. rlang::eval_tidy(.) 
3. rlang::enquo(my_string) %>% rlang::call_modify(z = z) %>% rlang::eval_tidy() 
2. fun_B(z = "i am z", my_string = fun_A(my_string = "replace {x} and {y}.", 
    x = x, y = y)) 
1. fun_C_notok(x = "this", y = "that") 

Does somebody understand why glue() can't find 'x' in fun_C_notok() ??

1 Answers

I'm pretty sure this is a bug in glue, with a suggested fix here: https://github.com/tidyverse/glue/issues/231#issuecomment-951129873 . In the meantime, this appears to work: specify the environment explicitly, by modifying your funA to this:

fun_A <- function(my_string, ..., z = NULL){
  print(paste0("z value: ", as.character(z))) # do something with z here
  print(list(...)) # to test: print ... to check that all variables are still there

  e <- list2env(list(...))
  my_string <- glue::glue(my_string, .envir = e, ...)
  my_string
}
Related