Load all functions in one go using r box package

196 Views Asked by At

I am using the box package. In my project, I have a functions folder, with two functions saved in there: hello.r and goodbye.r

I can load them using the box package using:

box::use(functions/hello)
box::use(functions/goodbye)

The problem is that I potentially have dozens of .r files in the functions folder (one .r file for each function).

Is there a way of loading all the .r files in one go (whilst still using the box package) without having to repeat the box::use function many times?

3

There are 3 best solutions below

2
On BEST ANSWER

Since box::use uses Non-Standard Evaluation (NSE) you have to transform strings into unevaluated language objects:

rm(list = ls())
ls()
# character(0)

rex <- "\\.[rR]$"
fld <- "functions"

fs::dir_tree(fld)
# functions
# ├── goodbye.R
# └── hello.R

fns <- list.files(fld, rex) |>     ## read all files in the folder
  gsub(rex, "", x = _) |>          ## drop extension
  paste(fld, . = _, sep = "/") |>  ## pre-pend folder
  lapply(str2lang)                 ## transform string into language object

## or (`tools` is a base package)
# fns <- tools::list_files_with_exts(fld, "R") |> 
#   tools::file_path_sans_ext() |>
#   lapply(str2lang)

## call box::use with the the newly creataed list
do.call(box::use, fns)
ls()
# [1] "fld"     "fns"     "goodbye" "hello"   "rex" 

Update based on Friede's tip to use str2lang

2
On

For what it’s worth, it’s good practice to list the imports explicitly, rather than doing a catch-all of nested files. This was a conscious design decision of ‘box’, and working around it is generally not recommended.

That said, you don’t have to repeat the box::use() call, because box::use() supports multiple import declarations:

box::use(
  functions/hello,
  functions/goodbye,
)

This is the recommended syntax for multiple imports in the same scope.

Furthermore, you can make functions itself into a module, and make it export all nested submodules. This seems to be appropriate in your case anyway, since the actual user-facing module seems to be functions, not each individual file in the folder — the latter should be strictly an implementation detail of the module, the user should not know or care about it.

In that case, add an __init__.r file to the functions folder with the following code:

#' @export
box::use(
  ./hello,
  ./goodbye,
)

Now the user only needs a single import. However, function on its own is not a valid module name because module names (intentionally) need to be namespaced (just like GitHub projects need to be namespaced: you can’t just have a GitHub project box, it needs to be called (e.g.) klmr/box. So let’s say you have your module basil/functions (i.e. move the folder functions into a parent folder basil which is in the module search path), then you can use your module via

box::use(basil/functions[...])

The other solutions presented here work, but they are explicitly contravening the spirit of the ‘box’ package, and if you write modules this way you are not taking best advantage of the infrastructure that ‘box’ provides. I would only ever recommend using the other solutions as a quick hack while porting large legacy projects to ‘box’ — never as a long-term solution.

0
On

1) On Windows assume the .R files are all in the boxtest folder. This creates a new file boxtest.R in the current folder and then uses box::use

shell("copy boxtest\\*.R > boxtest.R")
box::use(./boxtest[...])  # or remove [...] to not export objects 

On Linux we can use this instead of the shell command above.

system("cat boxtest/*.R > boxtest.R")

This will also work on Windows if Rtools is installed and the usr\bin subdirectory is on the current Windows PATH.

2) Alternately do it all in R:

writeLines(unlist(Map(readLines, Sys.glob("boxtest/*.R")), "boxtest.R")
box::use(./boxtest[...]) # or remove [...] to not export objects

3) This also seems to work althoug hit uses a function that is not exported from box and gives a few warnings but it is short and is entirely in R:

Map(box:::box_source, Sys.glob("boxtest/*.R"))

or forget about box and just use source directly:

Map(source, Sys.glob("boxtest/*.R"))