rtidyversepurrrr-glue

stringr::str_glue() doesn't work with purrr::map()


Does anyone know why the below doesn't work? It seems like stringr::str_glue() isn't able find the file_name argument because it's inside of purrr::map(). How can I get around that?

library(tidyverse)

fn_render_ce_template <- function(
    template,
    file_name, 
    early_billing_cycle = NA,
    late_billing_cycle = NA,
    early_price = NA,
    late_price = NA,
    price_dollars = NA,
    price_cents = NA,
    year = NA
){
  template %>% 
    map_chr(str_glue) %>% 
    str_squish()
}

# this doesn't work
fn_render_ce_template(
  template = c('{file_name}_1', '{file_name}_2'), 
  file_name = 'test'
)
#> Error:
#> i In index: 1.
#> Caused by error:
#> ! object 'file_name' not found
#> Backtrace:
#>      x
#>   1. +-global fn_render_ce_template(...)
#>   2. | \-template %>% map_chr(str_glue) %>% str_squish()
#>   3. +-stringr::str_squish(.)
#>   4. | +-stringi::stri_trim_both(str_replace_all(string, "\\s+", " "))
#>   5. | \-stringr::str_replace_all(string, "\\s+", " ")
#>   6. |   \-stringr:::check_lengths(string, pattern, replacement)
#>   7. |     \-vctrs::vec_size_common(...)
#>   8. +-purrr::map_chr(., str_glue)
#>   9. | \-purrr:::map_("character", .x, .f, ..., .progress = .progress)
#>  10. |   +-purrr:::with_indexed_errors(...)
#>  11. |   | \-base::withCallingHandlers(...)
#>  12. |   +-purrr:::call_with_cleanup(...)
#>  13. |   \-stringr (local) .f(.x[[i]], ...)
#>  14. |     \-glue::glue(..., .sep = .sep, .envir = .envir)
#>  15. |       \-glue::glue_data(...)
#>  16. +-glue (local) `<fn>`("file_name")
#>  17. | +-.transformer(expr, env) %||% .null
#>  18. | \-glue (local) .transformer(expr, env)
#>  19. |   \-base::eval(parse(text = text, keep.source = FALSE), envir)
#>  20. |     \-base::eval(parse(text = text, keep.source = FALSE), envir)
#>  21. \-base::.handleSimpleError(...)
#>  22.   \-purrr (local) h(simpleError(msg, call))
#>  23.     \-cli::cli_abort(...)
#>  24.       \-rlang::abort(...)

# but this does?
file_name <- 'test'

fn_render_ce_template(
  template = c('{file_name}_1', '{file_name}_2'), 
  file_name = 'test'
)
#> [1] "test_1" "test_2"

Created on 2023-12-23 with reprex v2.0.2


Solution

  • Define an anonymous function

    The short answer is this is about the environment in which variables are evaluated. You can resolve it by defining an anonymous function in your map_chr() call:

    render_template <- function(template, file_name) {
        template |>
            map_chr(\(x) str_glue(x))
    }
    

    This returns the desired output:

    render_template(
        template = c("{file_name}_1", "{file_name}_2"),
        file_name = "test"
    )
    # [1] "test_1" "test_2"
    

    Explanation

    To get a sense of why this happens, check out the function definition for str_glue():

    str_glue <- function(..., .sep = "", .envir = parent.frame()) {
      glue::glue(..., .sep = .sep, .envir = .envir)
    }
    

    By default it looks in the parent frame. When you use purrr::map_chr() it ultimately calls purrr::map_(), and the parent frame is not the right frame.

    You can make your function manually specify the frame to get a sense of what is happening:

    render_template <- function(template, file_name, stack_frame_num) {
        template |>
            map_chr(\(x) str_glue(x, .envir = rlang::caller_env(stack_frame_num)))
    }
    

    If we call this with a stack frame number of 1 we get the global variable.

    file_name <- "global"
    render_template(
        template = "{file_name}_1",
        file_name = "local",
        stack_frame_num = 1
    )
    # [1] "global_1"
    

    The same applies to a stack frame number of 2. However, if we call it with a stack frame number of 3:

    render_template(
        template = "{file_name}_1",
        file_name = "local",
        stack_frame_num = 3
    )
    # [1] "local_1"
    

    Fortunately we do not need to manually pass the stack frame number, as defining an anonymous function within your function means the parent frame is the correct one, but hopefully this gives you a sense of why this behaviour occurs.