-
-
Notifications
You must be signed in to change notification settings - Fork 47
/
README.rmd
171 lines (113 loc) · 8.97 KB
/
README.rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
<!-- README.md is generated from README.rmd. Please edit that file instead! -->
```{r echo = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = '#>',
fig.path = 'man/figures/'
)
options(box.path = 'inst')
box::use(./vignettes/source_file)
desc = read.dcf('DESCRIPTION')
desc = setNames(as.list(desc), colnames(desc))
```
# `r desc$Package` <img src="man/figures/logo.png" align="right" alt="" width="120"/>
> `r desc$Title`
[![CRAN status badge](https://www.r-pkg.org/badges/version/box)][CRAN]
[![R-universe status badge](https://klmr.r-universe.dev/badges/box)][R-universe]
<div id='tldr'>
* [Get started][]
* [Documentation][]
* [Contributing][]
* [Frequently asked questions][FAQ]
</div>
## 📦 Installation
‘box’ can be installed from CRAN:
```{r eval = FALSE}
install.packages('box')
```
Alternatively, the current development version can be installed from [R-universe][] (note that it *cannot* be installed directly from GitHub!):
```{r eval = FALSE}
install.packages('box', repos = 'https://klmr.r-universe.dev')
```
## 🥜 Usage in a nutshell
‘box’ allows organising R code in a more modular way, via two mechanisms:
1. It enables *writing modular code* by treating files and folders of R code as independent (potentially nested) modules, without requiring the user to wrap reusable code into packages.
2. It provides a new syntax to import reusable code (both from packages and modules) that is more powerful and less error-prone than `library` by allowing explicit control over what names to import, and by restricting the scope of the import.
### Reusable code modules
Code doesn’t have to be wrapped into an R package to be reusable. With ‘box’, regular R files are reusable **R modules** that can be used elsewhere. Just put the **export directive** `#' @export` in front of names that should be exported, e.g.:
```{r box_file = 'inst/mod/hello_world.r'}
```
Existing R scripts without `@export` directives can also be used as modules. In that case, all names inside the file will be exported, unless they start with a dot (`.`).
Such modules can be stored in a central **module search path** (configured via `options('box.path')`) analogous to the R package library, or locally in individual projects. Let’s assume the module we just defined is stored in a file `hello_world.r` inside a directory `mod`, which is inside the module search path. Then the following code imports and uses it:
```{r}
box::use(mod/hello_world)
hello_world$hello('Ross')
```
Modules are a lot like packages. But they are easier to write and use (often without requiring any set-up), and they offer some other nice features that set them apart from packages (such as the ability to be nested hierarchically).
For more information on writing modules refer to the *[Get started][]* vignette.
### Loading code
`box::use` provides a **universal import declaration**. It works for packages just as well as for modules. In fact, ‘box’ completely replaces the base R `library` and `require` functions. `box::use` is more explicit, more flexible, and less error-prone than `library`. At its simplest, it provides a direct replacement:
Instead of
```{r eval = FALSE}
library(ggplot2)
```
You’d write
```{r eval = FALSE}
box::use(ggplot2[...])
```
This tells R to import the ‘ggplot2’ package, and to make all its exported names available (i.e. to “attach” them) — just like `library`. For this purpose, `...` acts as a wildcard to denote “all exported names”. However, attaching everything is generally *discouraged* (hence why it needs to be done explicitly rather than happening implicitly), since it leads to name clashes, and makes it harder to retrace which names belong to what packages.
Instead, we can also instruct `box::use` to not attach any names when loading a package — or to just attach a few. Or we can tell it to attach names under an alias, and we can also give the package *itself* an alias.
The following `box::use` declaration illustrates these different cases:
```{r eval = FALSE}
box::use(
purrr, # 1
tbl = tibble, # 2
dplyr = dplyr[filter, select], # 3
stats[st_filter = filter, ...] # 4
)
```
**Users of Python, JavaScript, Rust and many other programming languages will find this `use` declaration familiar** (even if the syntax differs):
The code
1. *imports* the package ‘purrr’ (but does not attach any of its names);
2. creates an *alias* `tbl` for the imported ‘tibble’ package (but does not attach any of its names);
3. *imports* the package ‘dplyr’ and additionally *attaches* the names `dplyr::filter` and `dplyr::select`; and
4. *attaches* all exported names from ‘stats’, but uses the local *alias* `st_filter` for the name `stats::filter`.
Of the four packages loaded in the code above, only ‘purrr’, ‘tibble’ and ‘dplyr’ are made available by name (as `purrr`, `tbl` and `dplyr`, respectively), and we can use their exports via the `$` operator, e.g. `purrr$map` or `tbl$glimpse`. Although we’ve also loaded ‘stats’, we did not create a local name for the package itself, we only attached its exported names.
Thanks to aliases, we can safely use functions with the same name from multiple packages without conflict: in the above, `st_filter` refers to the `filter` function from the ‘stats’ package; by contrast, plain `filter` refers to the ‘dplyr’ function. Alternatively, we could also explicitly qualify the package alias, and write `dplyr$filter`.
Furthermore, unlike with `library`, the effects of `box::use` are restricted to the current scope: we can load and attach names *inside* a function, and this will not affect the calling scope (or elsewhere). So importing code happens *locally*, and functions which load packages no longer cause global side effects:
```{r}
log = function (msg) {
box::use(glue[glue])
# We can now use `glue` inside the function:
message(glue('[LOG MESSAGE] {msg}'))
}
log('test')
# … But `glue` remains undefined in the outer scope:
glue('test')
```
This makes it easy to encapsulate code with external dependencies without creating unintentional, far-reaching side effects.
‘box’ itself is never loaded via `library`. Instead, its functionality is always used explicitly via `box::use`.
## Getting help
If you encounter a bug or have a feature request, please [post an issue report on GitHub][new-issue]. For general questions, posting on Stack Overflow, [tagged as <span class="so-tag">r-box</span>][so:r-box], is also an option. Finally, there’s a [GitHub Discussions][] board at your disposal.
## Why ‘box’?
‘box’ makes it drastically easier to *write reusable code*: instead of needing to create a package, each R code file *is already a module* which can be imported using `box::use`. Modules can also be nested inside directories, such that self-contained projects can be easily split into separate or interdependent submodules.
To make code reuse more scalable for larger projects, ‘box’ promotes the opposite philosophy of what’s common in R: some notable packages export and attach many hundreds and, in at least one notable case, *over a thousand* names. This works adequately for small-ish analysis scripts but breaks down for even moderately large software projects because it makes it non-obvious where names are imported from, and increases the risk of name clashes.
To make code more explicit, readable and maintainable, software engineering best practices encourage limiting both the scope of names, as well as the number of names available in each scope.
For instance, best practice in Python is to never use the equivalent of `library(pkg)` (i.e. `from pkg import *`). Instead, Python [strongly encourages][pep8] using `import pkg` or `from pkg import a, few, symbols`, which correspond to `box::use(pkg)` and `box::use(pkg[a, few, symbols])`, respectively. The same is true in many other languages, e.g. [C++][], [Rust][] and [Perl][]. Some languages (e.g. JavaScript) are even stricter: they don’t support unqualified wildcard imports at all.
[*The Zen of Python*][pep20] puts this rule succinctly:
> **Explicit is better than implicit.**
[CRAN]: https://cran.r-project.org/package=box
[R-universe]: https://klmr.r-universe.dev/
[roxygen2]: https://roxygen2.r-lib.org/
[pep8]: https://www.python.org/dev/peps/pep-0008/#imports
[Get started]: https://klmr.me/box/articles/box.html
[Documentation]: https://klmr.me/box/reference/index.html
[Contributing]: https://klmr.me/box/articles/contributing.html
[FAQ]: https://klmr.me/box/articles/faq.html
[new-issue]: https://github.com/klmr/box/issues/new/choose
[so:r-box]: https://stackoverflow.com/questions/tagged/r-box?tab=Newest
[GitHub Discussions]: https://github.com/klmr/box/discussions
[C++]: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rs-using
[Rust]: https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#the-glob-operator
[Perl]: https://perldoc.perl.org/Exporter#Selecting-What-to-Export
[pep20]: https://www.python.org/dev/peps/pep-0020/