klmr / box

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

Import directories #318

Closed dereckmezquita closed 1 year ago

dereckmezquita commented 1 year ago

Please describe your feature request

Desired usage

Typically I use a modules folder. There I store separate files or directories that house like modules. Currently working with shiny so I would like to be able to do something like this:

And then import as such:

main.R

box::use(./modules/shiny_modules)

shiny_modules$some_server_and_ui_func$server_func()
shiny_modules$some_server_and_ui_func$ui_func()

shiny_modules$some_other_server_and_ui_func$server_func()
shiny_modules$some_other_server_and_ui_func$ui_func()

The purpose is to be able to group modules by type/logic and easily import them.

Current solution

As of now, I have to do this to accomplish something like this:

shiny_modules.R

box::use(./shiny_modules/some_server_and_ui_func)

#' @export
server_func <- some_server_and_ui_func$server_func

#' @export
ui_func <- some_server_and_ui_func$ui_func

main.R

box::use(./modules/shiny_modules)

shiny_modules$server_func()
shiny_modules$ui_func()

Instead I'd like to be able to do this:

main.R

box::use(./modules/shiny_modules)

Of course, when doing this, I have to use unique names; open to discussion and suggestions.

ArcadeAntics commented 1 year ago

This sounds like something you could accomplish with an init script named ./modules/shinymodules/__init_\.R with contents like:

box::use(
    ./some_server_and_ui_func,
    ./some_other_server_and_ui_func
)
box::export(
    some_server_and_ui_func,
    some_other_server_and_ui_func
)

You could then easily write:

box::use(./modules/shiny_modules)
shiny_modules$some_server_and_ui_func$servec_func()
shiny_modules$some_other_server_and_ui_func$ui_func()

and it would work because box::use()-ing a directory imports the init script as a module.

It would be great if there was method of delaying the usage of a module until it is needed, something like:

delayedAssign("some_server_and_ui_func", {
    loadModule(./some_server_and_ui_func)
})
delayedAssign("some_other_server_and_ui_func", {
    loadModule(./some_other_server_and_ui_func)
})
box::export(
    some_server_and_ui_func,
    some_other_server_and_ui_func
)

but there isn't. That being said, the method I wrote above is good enough. I hope this helps!

dereckmezquita commented 1 year ago

Yes, thank you again @ArcadeAntics this works nicely. The only friction I have is that autocompleting the functions inside that module doesn't work in VSCode (my primary editor). Autocomplete does work in RStudio though.

Thank you for the package @klmr, and you @ArcadeAntics - I keep commenting because I'm heavily using it! I think box as a package elevates R as a programming language. A sound modules system was severely missing from R!

klmr commented 1 year ago

Note that you can use @export directives directly on box::use() declarations. So your code can be written as:

shiny_modules.R:

#' @export
box::use(
  ./shiny_modules/some_server_and_ui_func[
    server_func
    ui_func
  ]
)

Putting @export on a box::use declaration causes exactly those names to be exported which are made available by box::use. So, in the above case, server_func and ui_func. Of course you can also use wildcard import (...) which avoids having to spell out all exports manually.

Incidentally, the “canonical” style would be to have a file shiny_modules/__init__.R instead of shiny_modules.R. This has the advantage that you don’t need to repeat the shiny_modules name inside the box::use declaration:

.
├── main.R
└── modules/
    ├── shiny_modules/
    │   ├── __init__.R
    │   ├── some_other_server_and_ui_func.R
    │   └── some_server_and_ui_func.R
    └── some_module.R

shiny_modules/__init__.R

#' @export
box::use(./some_server_and_ui_func[...])

This should be pretty close to what you wanted to achieve. Now you can write the following in main.R:

box::use(./modules/shiny_modules)

shiny_modules$server_func()
shiny_modules$ui_func()

If I understand correctly, your specific request is that the above should work without necessitating the need for the shiny/__init__.R (or shiny_modules.R) file, right? The reason why it is required is that ‘box’ encourages explicitness in defining a module’s public interface, and this would get lost if submodules were automatically exported (not to mention potential name clashes, as you mentioned). I hope you agree that the shortened shiny/__init__.R file I showed makes it less cumbersome to have to be explicit about exports.

Regarding @ArcadeAntics’ suggestion about delayed loading: this is worth keeping in mind but I am also working on a byte-compilation cache which would make module loading faster anyway, so I am not sure having delayed loading would have a benefit. In addition, delayed loading would mess with the initialisation order of modules (i.e. the order in which the .on_load hooks get called) and I am not sure how to fix that.

dereckmezquita commented 1 year ago

Exactly what I wanted thank you @klmr