klmr / box

Write reusable, composable and modular R code
https://klmr.me/box/
MIT License
862 stars 48 forks source link

Ability to re-export functions that have been loaded programmatically #350

Closed vorpalvorpal closed 9 months ago

vorpalvorpal commented 10 months ago

Please describe your feature request

This is almost certainly against the whole design philosophy of {box}, and yet...

As shown in issue 68, it is possible to load a bunch of modules programmatically, but it doesn't seem to then be possible to re-export them.

Background I have an r project. In it I have a folder named "R" that contains R scripts. I would like to have all functions defined in this directory that have an @export tag available to all other functions. Yes, I realise that this is not explicit, and is frowned upon, however this is an old project and there are a lot of functions defined that call each other.

Partial solution Based on issue 68, I can create an __init__.R file in ./R and have it include the following:

box::use(magrittr[`%>%`])
imports <- lapply(c('...'), as.name)

list.files(path = here::here("R"), 
           pattern = "\\.R$",
           full.names = FALSE,
           recursive = TRUE,
           ignore.case = TRUE,
           include.dirs = TRUE) |>
  # remove any scripts/folders that include "__" in the name
  grep(pattern = "^.*(__).+$", 
       value = TRUE, 
       invert = TRUE) |>
  stringr::str_remove(pattern = "\\.R$") %>%
  stringr::str_c("./", .) |>
  purrr::map(\(mod){
    print(mod)
    mod_name <- as.name(mod)
    bquote(./.(mod_name)[..(imports)], splice = TRUE)
      }) |> 
  do.call(box::use, args = _)

And this loads the functions just fine, however if I then add box::use(PATH_TO_R_FOLDER) to the top of a script, none of those functions called by box::use in __init__.R are then available because there is not an export declaration in __init__.R.

If I add #' @export above list.files I (unsurprisingly) get an error, and if I add box::export(...) I also get an error.

I could write some logic to look through all the files found by list.files and search for "^(.*) <- function\(" and then export each of those function names, but that seems error-prone.

klmr commented 9 months ago

You can programmatically export names, using a similar workaround as for the programmatic import. As for import, there may in the future be an officially-supported, non-hacky way to achieve this. — But I can’t make promises.

Either way, the secret sauce is the function box::export() which can (but generally should not) be used instead of the declarative (“roxygen-like”) export style.

I’ll show an example below but allow me to add a few notes about your code:

  1. The declaration of imports <- lapply(c('...'), as.name) is unnecessary: if you want to use wildcard imports, use ... directly in the quoted expression, no need to splice in the list of imports:

    bquote(./.(mod_name)[...])
  2. I strongly advise against using here::here() with ‘box’ (and in general). This is because here:here() does generally not do what it advertises and only really works in RStudio project-based workflows, nowhere else.1 The fact that it works in your code is a pure coincidence — try changing your working directory, it will stop working. ‘box’ provides the function box::file(), which will continue to work even after you change your working directory. In almost all cases, you want to use box::file() instead of here::here(). In your specific case, use box::file() instead of here::here('R').

  3. Don’t call stringr::str_c('./', .): you end up with the quoted expression ./`./mod_name`, which is probably not what you expected, and I am actually now confused why this works — this is actually a bug in ‘box’ (and I will fix this bug, so this code will stop working in the future!).

Okay. Here’s how you can use box::export() with dynamic names. I am assuming that you want to export all imported names (otherwise, adjust the exports vector accordingly):

import_env <- parent.env(box::topenv())
exports <- setdiff(ls(import_env, all.names = TRUE), '%>%')

do.call(box::export, lapply(exports, as.name))

Please note that I have just found a bug in box::export() which makes this fail for non-syntactic names (e.g. if you attempt to add %>% to exports you will get an error).


1 For more details please see the footnote in my comment at https://github.com/klmr/box/discussions/328#discussioncomment-6521485

vorpalvorpal commented 9 months ago

Thank you for your very generous response. I will try this out.

I realize that this is all contrary to the philosophy of box, but it seems necessary when trying to migrate a big messy existing project over to using box.

Thank you again.