The mod
package is a lightweight module system; It
provides a simple way to structure program and data into modules for
programming and interactive use, without the formalities of R
packages.
This is a good question.
As units for code organization in R, packages are very robust. However, they are formal and compliated; they require additional knowledge to R and must be installed in the local library. Scripts, as widely understood, are simplistic and brittle, unsuitable for building tools and may conflict with each other.
Situated between packages and scripts, modules feature characteristics that are somewhat similar to those from other languages. They can be defined either inline with other codes or in a standalone file, and can be used in the user’s working environment, packages, or other modules.
Let’s see.
Install the development version from GitHub with:
::install_github("iqis/mod") devtools
The mod
package is designed to be used either attached
or unattached to your working environment.
If you wish to attach the package:
require(mod)
#> Loading required package: mod
#>
#> Attaching package: 'mod'
#> The following object is masked from 'package:base':
#>
#> drop
module()
/mod::ule()
acquire()
use()
drop()
provide()
require()
refer()
Define an inline module:
<- module({
my <- 1
a <- 2
b <- function(x, y) x + y
f })
The resulting module contains the variables defined within.
ls(my)
#> [1] "a" "b" "f"
Subset the module.
$a
my#> [1] 1
$b
my#> [1] 2
$f(my$a, my$b)
my#> [1] 3
Use the with()
to spare qualification.
with(my,
f(a,b))
#> [1] 3
Just like a package, a module can be attached to the search path.
use(my)
The my
module is attached to the search path as
“module:my”, before other attached packages.
search()
#> [1] ".GlobalEnv" "module:my" "package:mod"
#> [4] "package:stats" "package:graphics" "package:grDevices"
#> [7] "package:utils" "package:datasets" "package:methods"
#> [10] "Autoloads" "package:base"
And you can use the variables inside directly, just like those from a package.
f(a,b)
#> [1] 3
Detach the module from the search path when done, if desired.
drop("my")
Use refer()
to “import” variables from another
module.
ls(my)
#> [1] "a" "b" "f"
<- module({
my_otherrefer(my)
<- 4
c <- 5
d
})
ls(my_other)
#> [1] "a" "b" "c" "d" "f"
The mod::require()
makes packages available for use in a
module.
<- module({
mpg_analysis require(ggplot2)
<- qplot(mtcars$mpg)
plot
})#> Registered S3 methods overwritten by 'ggplot2':
#> method from
#> [.quosures rlang
#> c.quosures rlang
#> print.quosures rlang
$plot
mpg_analysis#> `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
Meanwhile, your working environment’s search path remain unaffected:
search()
#> [1] ".GlobalEnv" "package:mod" "package:stats"
#> [4] "package:graphics" "package:grDevices" "package:utils"
#> [7] "package:datasets" "package:methods" "Autoloads"
#> [10] "package:base"
"package:ggplot2" %in% search()
#> [1] FALSE
A variable is private if its name starts with
..
.
<- module({
room_101 <- "Dear Diary: I used SPSS today..."
..diary <- function(){
get_diary
..diary
} })
A private variable cannot be seen or touched. There is no way to
access the ..diary
from the outside, except by a function
defined within the module. This can be useful if you want to shield some
information from the user or other programs.
ls(room_101)
#> [1] "get_diary"
$..diary
room_101#> NULL
$get_diary()
room_101#> [1] "Dear Diary: I used SPSS today..."
Another way is using provide()
function to declair
public variables, while all others become private.
<- module({
room_102 provide(open_info, get_classified)
<- "I am a data scientist."
open_info <- "I can't get the database driver to work."
classified_info <- function(){
get_classified
classified_info
}
})
ls(room_102)
#> [1] "get_classified" "open_info"
$open_info
room_102#> [1] "I am a data scientist."
$classified_info
room_102#> NULL
$get_classified()
room_102#> [1] "I can't get the database driver to work."
The below example simulates one essential behavior of an object in
Object-oriented Programming by manipulating the state of
..count
.
<- module({
counter <- 0
..count <- function(){
add_one #Its necessary to use `<<-` operator, as ..count lives in the parent frame.
<<- ..count + 1
..count
}<- function(){
reset <<- 0
..count
}<- function(){
get_count
..count
} })
A variable must be private to be mutable like
..count
.
The following demonstration should be self-explanatory:
$get_count()
counter#> [1] 0
$add_one()
counter$add_one()
counter
$get_count()
counter#> [1] 2
$reset()
counter
$get_count()
counter#> [1] 0
It is imperative that mod
be only adopted in the
simplest cases. If full-featured OOP is desired, use R6
.
A module is an environment. This means that every rule that applies to environments, such as copy-by-reference, applies to modules as well.
mode(my)
#> [1] "environment"
is.environment(my)
#> [1] TRUE
Some may wonder the choice of terms. Why refer()
and
provide()
? Further, why not import()
and
export()
? This is because we feel import()
and
export()
are used too commonly, in both R, and other
popular languages with varying meanings. The reticulate
package also uses import()
. To avoid confusion, we decided
to introduce some synonyms. With analogous semantics, refer()
is borrowed from Clojure, while provide()
from Racket; Both languages are R’s close relatives.
If a module is locked, it is impossible to either change the value of a variable or add a new variable to a module. A private variable’s value can only be changed by a function defined within the module, as shown previously.
$a <- 1
my#> Error in my$a <- 1: cannot change value of locked binding for 'a'
$new_var <- 1
my#> Error in my$new_var <- 1: cannot add bindings to a locked environment
As a general R rule, names that start with .
define
hidden variables.
<- module({
my_yet_another <- "I'm hidden!"
.var })
Hidden variables are not returned by ls()
, unless
specified.
ls(my_yet_another)
#> character(0)
ls(my_yet_another, all.names = TRUE)
#> [1] ".var"
Nonetheless, in a module, they are treated the same as public variables.
$.var
my_yet_another#> [1] "I'm hidden!"
<- system.file("misc/example_module.R", package = "mod") module_path
To load and assign to variable:
<- acquire(module_path)
example_module ls(example_module)
#> [1] "a" "d" "e"
$a
example_module#> [1] 1
$d()
example_module#> [1] 6
$e(100)
example_module#> [1] 106
To load and attach to search path:
use(module_path)
ls("module:example_module")
#> [1] "a" "d" "e"
a#> [1] 1
d()
#> [1] 6
e(100)
#> [1] 106
It could be confusing how modules and packages live together. To clarify:
require()
use()
refer()
Depends
on mod
packageAs aforementioned, the package is designed to be usable both attached and unattached.
If you use the package unattached, you must always qualify the
variable name with ::
, such as mod::ule()
, a
shorthand for mod::module()
. However, while inside a
module, the mod
package is always available, so you do not
need to use ::
. Note that in the following example,
provide()
inside the module expression is unqualified.
See:
detach("package:mod")
<- mod::ule({
my_mind provide(good_thought)
<- "I love working on this package!"
good_thought <- "I worry that no one will appreciate it."
bad_thought
})
::use(my_mind)
mod
good_thought#> [1] "I love working on this package!"