box: R modules and an import syntax that stays in scope
Write reusable, composable and modular R code
At a glance
- What is it?
- box is an R package that does two things at once: it lets any R file act as an importable module without being wrapped into a package, and it replaces library with a use declaration that is explicit about which names you import, lets you alias them, and confines them to the current scope.
- Who is it for?
- box is worth adopting in an R codebase that has grown a pile of loose scripts, or that keeps hitting name clashes between packages, because the module mechanism removes the packaging overhead and the scoped import removes the side effects. It is a poor fit for a small analysis script where a library call does the job, or for a team that has standardised on the tidyverse conventions, since the use syntax is a deliberate break from them.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 67 days ago.
- What is it written in?
- Mainly R, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two mechanisms, not one
The package describes itself as a way to write reusable, composable and modular R code, and the readme splits that into two mechanisms that are independent of each other.
The first is writing modular code. It treats files and folders of R code as independent and potentially nested modules, without requiring the user to wrap reusable code into packages. The second is a new import syntax that is more powerful and less error-prone than library, because it allows explicit control over what names to import and restricts the scope of the import.
That split is the design. The module mechanism is about how you organise the code you write, and the import syntax is about how you consume code from anywhere, whether that code is a module you wrote last week or a package from CRAN.
The argument for the first is made in the readme in terms of friction. Making a file reusable in R normally means making a package, and packaging is a ceremony with a directory structure, a description file and a namespace. box removes that ceremony, so a file with a few functions in it is already importable.
The argument for the second is about R's default behaviour, which the readme is direct about. Attaching everything a package exports is generally discouraged, because it leads to name clashes and makes it harder to retrace which names belong to what. In R the practice is nonetheless everywhere, so box makes the discouraged thing explicit and the recommended thing easy.
Any R file is already a module
The mechanism is a directive in a comment.
Put the export directive in front of the names that should be exported, and the file becomes a module with a defined public surface:
#' @export
hello = function (name) {
message('Hello, ', name, '!')
}Existing scripts that have no such directives also work. In that case all the names inside the file are exported, unless they start with a dot. That fallback is what makes box usable on an existing codebase, because you do not have to annotate a thousand files before you can import one of them.
Where the module lives is up to you. Modules can be stored in a central module search path, configured with an option named box.path, which is analogous to the R package library, or they can sit inside an individual project. The import then refers to the module by its path:
box::use(mod/hello_world)The readme notes that modules are a lot like packages but easier to write and use, often without requiring any set-up, and that they offer a feature packages do not, which is the ability to be nested hierarchically. Nesting is what lets a self-contained project be split into submodules that know about each other without a directory of R files that all have to be loaded into one namespace.
A vignette called Get started covers the writing side in more detail, and the readme points to it rather than trying to summarise the whole format here.
A use declaration with four distinct behaviours
The import syntax is the part of the package that replaces a familiar R habit, and the readme is careful to say that box::use is a universal import declaration that works for packages just as well as for modules, and that it completely replaces the base R library and require functions.
At its simplest it is a direct replacement, with one difference:
box::use(ggplot2[...])The dots act as a wildcard meaning all exported names, and the effect is to attach them, exactly like library would. The point of requiring the dots is that attaching everything is discouraged, so the package makes you say it out loud.
Beyond that, four behaviours are distinguished. You can import a package without attaching any of its names, which is the default and the recommended form. You can give a package an alias. You can attach only the names you want, by listing them. And you can attach a name under a different local name, which is how you keep two functions called filter from different packages apart.
The readme spells out the result of a combined declaration: of the four packages loaded, only those that were given a name are reachable by that name, and their exports are used through the dollar operator, for example a map function on one package or a glimpse function on another. The package you attached names from but did not name itself is the one case where you get the names without the handle.
Users of Python, JavaScript and Rust will recognise the shape, and the readme says so.
Imports stay inside the function that makes them
This is the property that separates box from every other import mechanism in R, and it is the one that changes how you write functions.
The effects of box::use are restricted to the current scope. You can load and attach names inside a function, and that will not affect the calling scope or anywhere else. So importing code happens locally, and a function that loads packages no longer causes global side effects.
The readme demonstrates it with a logging helper that pulls in a string interpolation library, uses it, and leaves the outer scope untouched, so calling the helper with the library name afterwards produces an error because the name is not defined there.
The consequence the readme draws is that it becomes easy to encapsulate code with external dependencies without creating unintentional, far-reaching side effects.
One more convention follows from the same idea. box itself is never loaded with library. Its functionality is always used explicitly through box::use, so the package that provides the import mechanism stays out of your search path and cannot collide with anything.
For a language server or a static analyser, the explicit form is also easier to reason about, because a name appearing in a file is either defined there or attached by a declaration in that file.
Installing from CRAN, or from R-universe but not from GitHub
The installation section is short, and it contains one instruction that will save you an hour if you skip it.
The package is on CRAN, and the standard install works:
install.packages('box')The current development version comes from R-universe, which is the author's own package repository:
install.packages('box', repos = 'https://klmr.r-universe.dev')The parenthetical note next to that is the important part. The development version cannot be installed directly from GitHub. That is a real constraint, not a style preference: the repository does have a src directory with C sources alongside the R directory, and the R-universe path is the supported way to get the development build.
There is also an R-universe badge, so the development repository is a first class distribution channel rather than a side project.
For support, the readme points at the issue tracker for bugs and feature requests, a Stack Overflow tag for general questions, and a GitHub Discussions board.
What is in the repository, and what the Makefile does
The repository is laid out like a serious R package, which it is.
There is a DESCRIPTION file, a licence file, an R directory for the R sources, a src directory for the C sources, a tests directory, a vignettes directory, a man directory for the generated documentation, an inst directory, a cran directory, a pkgdown directory for the website, a scripts directory, and a Makefile. There is a lintr configuration, a news file, and both a README file and the R Markdown source it is generated from, with a comment at the top of the readme saying that it is generated and that the source file is the one to edit.
The Makefile targets describe the workflow. The test target runs the unit tests through the devtools test function with export all turned off, and there is a pattern target that runs a single named test. The check target runs a set of rule scripts from the scripts directory, each of which can fail the build, and then runs the standard R package check into a check directory, combining the exit statuses. There is a site target that builds the package website, and the documentation is a dependency of the test and check targets, so the docs are regenerated before the tests run rather than drifting.
The Makefile also shows a care for the R installation itself. It resolves the R home directory through Rscript, and every invocation unsets the R_BOX_PATH environment variable, which means the tests and checks run without whatever module search path the developer happens to have set in their shell. For a package whose entire purpose is changing how imports resolve, that is a sensible thing to be strict about.
Editorial conclusion
box is worth adopting in an R codebase that has grown a pile of loose scripts, or that keeps hitting name clashes between packages, because the module mechanism removes the packaging overhead and the scoped import removes the side effects. It is a poor fit for a small analysis script where a library call does the job, or for a team that has standardised on the tidyverse conventions, since the use syntax is a deliberate break from them. Before you convert anything, check that your project can live with imports that do not leak out of a function, and remember that the package cannot be installed from GitHub directly, so pick CRAN or R-universe.
Frequently asked questions
How do I install the box package for R?
From CRAN with install.packages('box'), or the current development version from R-universe with install.packages('box', repos = 'https://klmr.r-universe.dev'). The readme notes that the development version cannot be installed directly from GitHub.
Do R files need to be packages to be reusable with box?
No. box treats files and folders of R code as independent and potentially nested modules. You can mark the exported names with the @export directive, and files without any such directive export all their names except those starting with a dot, so existing scripts work as modules with no changes.
How does box::use differ from library?
box::use is explicit about what is attached, it can alias a package or a name so two functions with the same name do not clash, and its effects are restricted to the current scope, so a function that imports a package no longer changes the calling scope. The package also says that box itself is never loaded with library and is always used explicitly.
Where does box look for modules?
Modules can live in a central module search path configured with the box.path option, which is analogous to the R package library, or they can sit inside an individual project. They are imported by path, for example box::use(mod/hello_world), and they can be nested hierarchically inside directories.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/klmr-box)