Search code examples
reval

Using strings as arguments of a function


There is plenty of questions on this topic on SO, including this question on how to evaluate expressions, this one showing some disagreement about using eval, and this answer showing the eval(substitute(.)) trick. A good reference to learn about non-standard evaluation (NSE) is the metaprogramming section of Advanced R, by Hadley Wickham. I note a lot of disagreement in the answers, with some signalling the problems of using strings as something to be evaluated.

I am in this condition of using a character string as an argument for a child function.
Here is a reprex:

# Consider a little toy function:
little_fun <- function(z = 2, y = 1) {
     (z + 2) / y
}
little_fun()
#> [1] 4

# I can call little_fun() on lists of arguments:
z_list <- c(1,2,3)
purrr::map_dbl(.x = z_list, ~ little_fun(z = (.x)))   # This is basically a tidyverse equivalent for lapply()
#> [1] 3 4 5

# or also:
z_list <- c(1,2,3)
y_list <- c(-1, 0, 1)
purrr::map2_dbl(.x = z_list, .y = y_list, ~ little_fun(z = (.x), y = (.y)))  # again, similar to mapply()
#> [1]  -3 Inf   5

# But I also want to assign the parameters from a more general parent function:
big_fun <- function(par = "y") {

     stopifnot(par %in% c("z", "y"))

     par_list <- c(1,2,3)
     purrr::map_dbl(.x = par_list, ~ little_fun(par = (.x)))   # <--- key line <---
}
big_fun()
#> Error in little_fun(par = (.x)): unused argument (par = (.x))

My problem: I am still unable to get my code running.

My question: why is it bad to use characters as function arguments? Should I avoid this? and how? I would like to understand how to improve my reasoning and learn available alternatives.


Solution

  • I don't disagree with chinsoon12's answer to your question of "Should I avoid this?"

    "protect yourself against future stupidity" – chinsoon12

    Having said that, dynamically-assigned parameter names is one place where do.call comes in handy.

    big_fun <- function(par = "y") {
         stopifnot(par %in% c("z", "y"))
         par_list <- c(1,2,3)
         purrr::map_dbl(.x = par_list, ~do.call(little_fun, as.list(setNames(.x, par))))
    }
    
    big_fun()
    # [1] 4.000000 2.000000 1.333333
    big_fun("z")
    # [1] 3 4 5
    

    I personally find programming like this at times mind-warping and problematic, and at others perhaps the only way to solve some problem elegantly. Sometimes it is the most terse, robust, and efficient solution available.

    And just to check, I believe big_fun() by itself should match this:

    sapply(1:3, little_fun, z=2)
    # [1] 4.000000 2.000000 1.333333