In this vignette you can find details on
Modules are first class citizens in the sense that they can be treated like any other data structure in R:
Modules are represented as list type in R. Such that
library("modules")
m <- module({
foo <- function() "foo"
})
is.list(m)
#> [1] TRUE
class(m)
#> [1] "module" "list"
S3 methods may be defined for the class module. The package itself only implements a method for the generic function print
.
Nested modules are modules defined inside other modules. In this case dependencies of the top level module are accessible to its children:
m <- module({
import("stats", "median")
anotherModule <- module({
foo <- function() "foo"
})
bar <- function() "bar"
})
getSearchPathContent(m)
#> List of 5
#> $ modules:root : chr [1:2] "anotherModule" "bar"
#> $ modules:stats : chr "median"
#> $ modules:internals: chr [1:10] "attach" "depend" "export" "expose" ...
#> $ base : chr [1:1243] "!" "!.hexmode" "!.octmode" "!=" ...
#> $ R_EmptyEnv : chr(0)
#> - attr(*, "class")= chr [1:2] "SearchPathContent" "list"
getSearchPathContent(m$anotherModule)
#> List of 7
#> $ modules:root : chr "foo"
#> $ modules:internals: chr [1:10] "attach" "depend" "export" "expose" ...
#> $ modules:root : chr [1:2] "anotherModule" "bar"
#> $ modules:stats : chr "median"
#> $ modules:internals: chr [1:10] "attach" "depend" "export" "expose" ...
#> $ base : chr [1:1243] "!" "!.hexmode" "!.octmode" "!=" ...
#> $ R_EmptyEnv : chr(0)
#> - attr(*, "class")= chr [1:2] "SearchPathContent" "list"
Sometimes it can be useful to pass arguments to a module. If you have a background in object oriented programming you may find this natural. From a functional perspective we define parameters shared by a list of closures. This is achieved by making the enclosing environment of the module available to the module itself.
amodule
is a wrapper around module
to abstract the following pattern:
m <- function(param) {
module(topEncl = environment(), {
fun <- function() param
})
}
m(1)$fun()
#> [1] 1
Using one of these approaches you construct a local namespace definition with the option to pass down some arguments.
This can be very useful to handle dependencies between two modules. Instead of:
which would hard code the dependency, we can write:
There are many good reasons to follow such a strategy. As an example: consider the case in which module a
introduces side effects. By leaving it open as argument we can later decide what exactly we pass down to the constructor of b
. This may be important to us when we want to mock a database, disable logging or otherwise handle access to external ressources.
You can not only put functions into your bag (module) but any R-object. This includes data: modules can be state-full. To illustrate this we define a module to encapsulate some value and have a get and set method for it:
mutableModule <- module({
.num <- NULL
get <- function() .num
set <- function(val) .num <<- val
})
mutableModule$get()
#> NULL
mutableModule$set(2)
In the next module we can use mutableModule
and rebuild the interface to .num
.
complectModule <- module({
suppressMessages(use(mutableModule, attach = TRUE))
getNum <- function() get()
set(3)
})
mutableModule$get()
#> [1] 2
complectModule$getNum()
#> [1] 3
Depending on your expectations with respect to the above code it comes at a surprise that we can get and set that value from an attached module; Furthermore it is not changed in mutableModule
. This is because use
will trigger a re-initialization of any module you plug in. You can override this behaviour:
In contrast to systems of object orientation, modules do not provide a formal mechanism of inheritance. Instead we can use various modes of composition. Inheritance often is used to reuse code; or to add functionality to an existing module.
In this context we may use parameterized modules, use
, expose
and extend
. The first two have already been discussed, as has been dependency injection as a strategy to encode relationships between modules.
expose
is most useful when we want to re-export functions from another module:
A <- function() {
amodule({
foo <- function() "foo"
})
}
B <- function(a) {
amodule({
expose(a)
bar <- function() "bar"
})
}
B(A())$foo()
#> [1] "foo"
B(A())$bar()
#> [1] "bar"
Here we can easily add functionality to a module, or only reuse parts of it. Another way to achieve this is to use extend
. The difference is, that with expose
we re-export existing functionality unchanged. With extend
we add lines of code to an existing module definition. This means we can (a) override private members of that module and (b) generally gain access to all implementation details. Hence the following two definitions are equivalent:
Variant A
a <- module({
foo <- function() "foo"
bar <- function() "bar"
})
a
#> bar:
#> function()
#>
#>
#> foo:
#> function()
Variant B
a <- module({
foo <- function() "foo"
})
a <- extend(a, {
bar <- function() "bar"
})
a
#> bar:
#> function()
#>
#>
#> foo:
#> function()
extend
should be used with great care. It is possible and easy to breake functionality of the module you extend. This is not possible or at least more challenging using expose
.
The real use case for extend
is to add unit tests to a module. You can think of using one of two patterns:
Variant A
Variant B
a <- module({
foo <- function() "foo"
})
extend(a, {
stopifnot(foo() == "foo")
})
#> foo:
#> function()
The latter alternative will keep the interface clean and gives access to private member functions. Sometimes this can be very useful for testing.
Of course a good way to write R code is to write packages. Modules inside of packages make a lot of sense, because also in a package we only have one scope to work with. Modules provide more options.
modules::module
: will connect to the packages namespace by default. Functions defined inside modules have access to the internal scope of the package.modules::amodule
: provides a slightly saver way and requires explicit registration of objects from the packages namespace. This can happen via dependency injection or modules::use
.If you write constructor functions for your modules (see example below) you automatically take advantage of R CMD check
. R CMD check
will provide some static code analysis tools which are generally helpful.
As you would avoid using library
inside of packages, you should also avoid using modules::import
. The R package namespace mechanism is more than capable of handling all dependencies.