1

I am developing an R package in which I have an exported function that needs to call several unexported functions and store the results from each in a list. Which functions are called is variable and depends on user input.

My approach to this was to lapply a (character) vector of function names with do.call, but it seems like this makes the unexported functions invisible to the exported function.

Consider the following example package code:

tmp1 <- function(x) print(paste("Function 1 called with x =", x))
tmp2 <- function(x) print(paste("Function 2 called with x =", x))
tmp3 <- function(x) print(paste("Function 3 called with x =", x))

#' @export
test1 <- function() {
  tmp1("test")
  tmp2("test")
  tmp3("test")
}

#' @export
test2 <- function() {
  funs <- c("tmp1", "tmp2", "tmp3")
  for (fun in funs) do.call(fun, list(x = "test"))
}

#' @export
test3 <- function() {
  funs <- c("tmp1", "tmp2", "tmp3")
  lapply(funs, do.call, list(x = "test"))
}

After building and loading the package, running the three test functions yield the following output:

test1()
#> [1] "Function 1 called with x = test"
#> [1] "Function 2 called with x = test"
#> [1] "Function 3 called with x = test"

test2()
#> [1] "Function 1 called with x = test"
#> [1] "Function 2 called with x = test"
#> [1] "Function 3 called with x = test"

test3()
#> Error in tmp1(x = "test"): could not find function "tmp1"

Calling the functions directly works, and calling them with do.call works when using do.call directly, but it fails when calling them via lapply. I can make a workaround with the for-loop, but I am curious as to why this happens.

So, my question is twofold:

  1. Why are the unexported functions invisible to do.call when called inside lapply?
  2. Can I make the lapply(funs, do.call, list(...)) approach work?
janusvm
  • 355
  • 2
  • 10
  • I can't reproduce your error. When I create a package with your code it works fine for me. But, anyway, I think when you're writing a package you should specify where these functions can be found using `::`. – Dan May 08 '18 at 11:55
  • Does using `funs <- c(tmp1, tmp2, tmp3)` instead of `funs <- c("tmp1", "tmp2", "tmp3")` solve your issue ? Or do `lapply(mget(funs), do.call, list(x = "test"))` which amounts to the same – moodymudskipper May 08 '18 at 12:02
  • @Moody_Mudskipper it makes `test3` run, but it doesn't answer my question (i.e. _why_ the `lapply` approach fails), and besides, in my actual package, `funs` will be a character vector specified (to some extent) by the user. – janusvm May 08 '18 at 12:08
  • Then using `mget` should solve it, though it doesn't answer why it doesn't work. Working with function objects is cleaner IMHO. – moodymudskipper May 08 '18 at 12:29
  • `do.call` evaluates by default its character arguments in `parent.frame()`, when using `lapply` you're doing some environment gymnastics that makes your code fail (unfortunately I don't know enough to say much more), so you can probably also solve your issue by tweaking the `envir` parameter. From `?do.call` : `envir an environment within which to evaluate the call. This will be most useful if what is a character string and the arguments are symbols or quoted expressions.` – moodymudskipper May 08 '18 at 12:34
  • @Moody_Mudskipper After playing around with the `envir` parameter, I found that: `lapply(funs, do.call, args = list(x = "test"), envir = environment())` runs correctly -- thanks! – janusvm May 08 '18 at 12:45

2 Answers2

0

This has partly been covered elsewhere, I recommend you to read these two(1, 2) threads here at stackoverflow on global variables and scoping assignment.

To solve your problem, add "<<-" when assigning your functions, i.e.

tmp1 <<- function(x) print(paste("Function 1 called with x =", x))
tmp2 <<- function(x) print(paste("Function 2 called with x =", x))
tmp3 <<- function(x) print(paste("Function 3 called with x =", x))

Output:

> test1()
[1] "Function 1 called with x = test"
[1] "Function 2 called with x = test"
[1] "Function 3 called with x = test"
> 
> test2()
[1] "Function 1 called with x = test"
[1] "Function 2 called with x = test"
[1] "Function 3 called with x = test"
> test3()
[1] "Function 1 called with x = test"
[1] "Function 2 called with x = test"
[1] "Function 3 called with x = test"
[[1]]
[1] "Function 1 called with x = test"

[[2]]
[1] "Function 2 called with x = test"

[[3]]
[1] "Function 3 called with x = test"
nadizan
  • 1,323
  • 10
  • 23
  • Doing this caused `devtools::document` to place definitions for the `tmp` functions in the global environment, which is definitely not what I want, and loading the package from a fresh session, `test3` still doesn't work (in fact, none of the `test` functions work) – janusvm May 08 '18 at 11:57
0

Following the comments on the question, I have a (partial) answer:

  1. do.call by default evaluates character arguments in parent.frame(), and when called inside lapply, this is a different environment than the one the tmp functions are defined in (though I'm not sure about the specifics)
  2. Supplying the current environment to do.call makes the lapply approach work: lapply(funs, do.call, args = list(x = "test"), envir = environment())
janusvm
  • 355
  • 2
  • 10