├── .Rbuildignore ├── .github └── workflows │ └── rcmdcheck.yml ├── .gitignore ├── .lintr ├── .projectile ├── .travis.yml ├── CRAN-SUBMISSION ├── DESCRIPTION ├── LICENSE ├── NAMESPACE ├── NEWS ├── R ├── NAMESPACE.R ├── amodule.R ├── base-override.R ├── class.R ├── depend.R ├── export.R ├── expose.R ├── extend.R ├── getSearchPath.R ├── import.R ├── module-class.R ├── module-coercion.R ├── module-helper.R ├── module.R ├── testModule.R └── use.R ├── README.md ├── cran-comments.md ├── man ├── amodule.Rd ├── depend.Rd ├── export.Rd ├── expose.Rd ├── extend.Rd ├── import.Rd ├── module.Rd ├── modulecoerce.Rd ├── use.Rd └── utilityFunctions.Rd ├── modules.Rproj ├── prepareRepo.R ├── tests ├── reattachModule.R ├── testModule.R ├── testthat.R └── testthat │ ├── test-amodule.R │ ├── test-base-override.R │ ├── test-depend.R │ ├── test-export.R │ ├── test-expose.R │ ├── test-extend.R │ ├── test-getSearchPathContent.R │ ├── test-getSearchPathDuplicates.R │ ├── test-import.R │ ├── test-lintr.R │ ├── test-model-coercion.R │ ├── test-module.R │ ├── test-print.R │ └── test-use.R └── vignettes ├── modulesAsFiles.Rmd ├── modulesAsObjects.Rmd └── modulesInR.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^prepareRepo\.R$ 4 | ^README\.md$ 5 | ^\.travis\.yml$ 6 | ^cran-comments\.md$ 7 | ^\.projectile$ 8 | ^\.github$ 9 | ^doc$ 10 | ^Meta$ 11 | ^\.lintr$ 12 | ^CRAN-RELEASE$ 13 | ^CRAN-SUBMISSION$ 14 | ^\.vscode$ 15 | -------------------------------------------------------------------------------- /.github/workflows/rcmdcheck.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: R-CMD-check 4 | 5 | jobs: 6 | R-CMD-check: 7 | runs-on: macos-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: r-lib/actions/setup-pandoc@v2 11 | - uses: r-lib/actions/setup-r@v2-branch 12 | - name: Install dependencies 13 | run: | 14 | install.packages(c("remotes", "rcmdcheck")) 15 | remotes::install_deps(dependencies = TRUE) 16 | shell: Rscript {0} 17 | - name: Check 18 | run: rcmdcheck::rcmdcheck(args = "--no-manual", error_on = "error") 19 | shell: Rscript {0} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | inst/doc 5 | vignettes/*.R 6 | vignettes/*.html 7 | doc 8 | Meta 9 | /doc/ 10 | /Meta/ 11 | -------------------------------------------------------------------------------- /.lintr: -------------------------------------------------------------------------------- 1 | linters: with_defaults( 2 | object_name_linter("camelCase"), 3 | line_length_linter(90) 4 | ) 5 | exclude: "# Exclude Linting" 6 | -------------------------------------------------------------------------------- /.projectile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wahani/modules/7578c5b6ef779bfd3e4d5e4da1a47189480ffc63/.projectile -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Sample .travis.yml for R projects 2 | 3 | language: r 4 | warnings_are_errors: true 5 | sudo: required 6 | 7 | r_github_packages: 8 | - jimhester/covr 9 | 10 | after_success: 11 | - Rscript -e 'library(covr);codecov()' 12 | -------------------------------------------------------------------------------- /CRAN-SUBMISSION: -------------------------------------------------------------------------------- 1 | Version: 0.13.0 2 | Date: 2024-01-20 15:43:08 UTC 3 | SHA: 489ab3464a2b6a1d99a7c7fa53ef8639e08ab0c3 4 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: modules 2 | Title: Self Contained Units of Source Code 3 | Version: 0.13.0 4 | Authors@R: person("Sebastian", "Warnholz", email = "wahani@gmail.com", role = c("aut", "cre")) 5 | Description: Provides modules as an organizational unit for source code. Modules 6 | enforce to be more rigorous when defining dependencies and have 7 | a local search path. They can be used as a sub unit within packages 8 | or in scripts. 9 | BugReports: https://github.com/wahani/modules/issues 10 | URL: https://github.com/wahani/modules 11 | ByteCompile: TRUE 12 | Depends: 13 | R (>= 3.2.0) 14 | Imports: 15 | utils 16 | License: MIT + file LICENSE 17 | Encoding: UTF-8 18 | Suggests: 19 | testthat, 20 | devtools, 21 | knitr, 22 | lintr, 23 | rmarkdown, 24 | parallel 25 | RoxygenNote: 7.1.2 26 | Collate: 27 | 'amodule.R' 28 | 'NAMESPACE.R' 29 | 'getSearchPath.R' 30 | 'class.R' 31 | 'depend.R' 32 | 'export.R' 33 | 'expose.R' 34 | 'extend.R' 35 | 'import.R' 36 | 'module-class.R' 37 | 'module-coercion.R' 38 | 'module-helper.R' 39 | 'module.R' 40 | 'use.R' 41 | 'testModule.R' 42 | 'base-override.R' 43 | VignetteBuilder: knitr 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2016-2023 2 | COPYRIGHT HOLDER: Sebastian Warnholz 3 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(as.module,"function") 4 | S3method(as.module,character) 5 | S3method(as.module,default) 6 | S3method(as.module,module) 7 | S3method(depend,default) 8 | S3method(print,SearchPathContent) 9 | S3method(print,module) 10 | export(amodule) 11 | export(as.module) 12 | export(autoTopEncl) 13 | export(depend) 14 | export(export) 15 | export(expose) 16 | export(extend) 17 | export(getSearchPath) 18 | export(getSearchPathContent) 19 | export(getSearchPathDuplicates) 20 | export(getSearchPathNames) 21 | export(import) 22 | export(importDefaultPackages) 23 | export(module) 24 | export(use) 25 | importFrom(utils,data) 26 | importFrom(utils,download.file) 27 | importFrom(utils,install.packages) 28 | importFrom(utils,installed.packages) 29 | importFrom(utils,packageVersion) 30 | importFrom(utils,str) 31 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | Version 0.13.0 2 | 3 | - 'export' now raises a warning when called from outside of a module instead of raising an error. 4 | #47 5 | 6 | Version 0.12.0 7 | 8 | - Bugfix when exporting object with special characters of length 1, e.g. `!` #45 9 | 10 | Version 0.11.0 11 | 12 | - Bugfix when exporting objects with whitespace in its name #39 13 | - Bugfix when exporting objects with special characters in its name #43 14 | 15 | Version 0.10.0 16 | 17 | - CRAN release 18 | - Bugfix when exporting objects with special names #37 19 | 20 | Version 0.9.0 21 | 22 | - CRAN release 23 | - Bugfix from issue #16 on Github: extend a module which has been loaded from a file now actually 24 | works 25 | - Extending the export mechanism to allow for renaming: see #19 26 | - Reattaching a module in the .GlobalEnv now actually works. See #24 27 | - Importing a complete package now also imports datasets. See #29 28 | - importDefaultPackages can be used to import Rs default packages, e.g. utils and stats. See #31 29 | 30 | Version 0.8.0 31 | 32 | - CRAN release 33 | - Update to documentation 34 | - Bugfix: find 'module' inside a module declaration 35 | - New function 'getSearchPathContent' to investigate the search path of a module 36 | - getSearchPath can now handle functions and modules as argument 37 | - New vignettes 38 | - 'use' and 'import' now signal conflicts using 'packageStartupMessage' instead of 'message'. They 39 | can now also be suppressed using 'suppressPackageStartupMessage'. 40 | - Skipping test for cross package dependencies on CRAN - not yet possible to reproduce locally. 41 | 42 | Version 0.7.0 43 | 44 | - CRAN release 45 | - New function 'depend' to declare package dependencies 46 | - Throw error when names are exported which are not available 47 | - New function 'amodule' aka module2 -> improved scoping of modules and dependency injection 48 | - The function 'use' will now find symbols defined in a top level environment 49 | - The function 'use' can now also handle URLs 50 | 51 | Version 0.6.0 52 | 53 | - CRAN release 54 | - Fixing conditional use of packages in suggests 55 | 56 | Version 0.5.2 57 | 58 | - Update documentation for parameterized modules. 59 | 60 | Version 0.5.1 61 | 62 | - New function: extend, can be used to extend an existing module definition. 63 | 64 | Version 0.5.0 65 | 66 | - CRAN release 67 | 68 | Version 0.4.4 69 | 70 | - Eliminated package dependencies: 'aoos' and 'methods' 71 | 72 | Version 0.4.3 73 | 74 | - Fix for print method. 75 | - Removing class and function 'modfun' 76 | 77 | Version 0.4.2 78 | 79 | - The function 'import' now has the additional argument 'attach' and returns an environment 80 | containing the specified imports. 81 | 82 | Version 0.4.1 83 | 84 | - Bugfix in expose when not called from within a module. 85 | 86 | Version 0.4.0 87 | 88 | - Cran release 89 | - Minor bugfixes 90 | - Update for testthat compatibility 91 | 92 | Version 0.3.1 93 | 94 | - 'export' now appends the vector of exports instead of replacing it. 95 | - Bugfix in 'export' for regexs. 96 | 97 | Version 0.3.0 98 | 99 | - CRAN release 100 | 101 | Version 0.2.4 102 | 103 | - 'import' now checks if a package dependency is installed instead of quietly waiting for a 104 | runtime error: fail early! 105 | 106 | Version 0.2.3 107 | 108 | - modules can now distinguish between situations in which they are constructed inside a package, 109 | inside another module (nested module), or inside a script. This behaviour is implemented in 110 | 'autoTopEncl' and can be overridden. This behaviour defines the search path of a module. 111 | 112 | Version 0.2.2 113 | 114 | - 'use' and 'expose' have different argument names. '...' can now be used to select objects to 115 | load / attach. The order of arguments has changed to be consistent with 'import'. 'reInit' can 116 | be used to trigger re-initialization of module. 117 | 118 | Version 0.2.1 119 | 120 | - When 'use' is called, modules are re initialized by default. 121 | - A 'module' now has an attribute 'moduleConst' which knows how to initialize a module and 122 | contains sufficient information for that process. 123 | - New function 'expose' to inherit a whole module. 124 | 125 | Version 0.2.0 126 | 127 | - CRAN release 128 | 129 | Version 0.1.4 130 | 131 | - ad-hoc documentation of functions using '##' as trigger. 132 | 133 | Version 0.1.3 134 | 135 | - as.module and thus use now accept a directory path. In this case all R-files in that directory 136 | are treated as module and returned as list of modules. 137 | 138 | Version 0.1.2 139 | 140 | - use and import now avoid duplicate dependencies on the search path 141 | - new functions: getSearchPath, getSearchPathNames to explore the local search path 142 | 143 | Version 0.1.1 144 | 145 | - fix when de-parsing imports 146 | 147 | Version 0.1.0 148 | 149 | - CRAN release 150 | 151 | Version 0.0.8 152 | 153 | - use does not attach by default 154 | - a regex in export is indicated by a leading "^" 155 | 156 | Version 0.0.5 157 | 158 | - exports 159 | 160 | Version 0.0.4 161 | 162 | - new function: as.module 163 | -------------------------------------------------------------------------------- /R/NAMESPACE.R: -------------------------------------------------------------------------------- 1 | #' @importFrom utils data download.file install.packages installed.packages 2 | #' packageVersion str 3 | NULL 4 | 5 | retList <- function(class = NULL, public = ls(envir), super = list(), envir = parent.frame()) { 6 | ## This is a variation of aoos::retList. Without the former inheritance 7 | ## mechanism. Maybe this with a different name is sufficient to supply 8 | ## OO-features in this package. 9 | public <- unique(c(public, names(super))) 10 | classes <- c(class, class(super)) 11 | envir$.self <- envir 12 | out <- super 13 | out[public] <- as.list(envir, all.names = TRUE)[public] 14 | class(out) <- classes 15 | out 16 | } 17 | -------------------------------------------------------------------------------- /R/amodule.R: -------------------------------------------------------------------------------- 1 | #' Define Augmented and Parameterized Modules 2 | #' 3 | #' \code{amodule} is a wrapper around \link{module} and changes the default 4 | #' environment to which the module connects. In contrast to \code{module} 5 | #' the top enclosing environment here is always \code{baseenv}. The second 6 | #' important difference is that the environment in which a module is created has 7 | #' meaning: all objects are made available to the module scope. This is 8 | #' what is meant by \emph{augmented} or \emph{parameterized}. Best practice for 9 | #' the use of this behavior is to return these modules from functions. 10 | #' 11 | #' @param expr (expression) a module declaration, same as \link{module} 12 | #' @param envir (environment) environment used to detect 'parameters' 13 | #' @param enclos (environment) the top enclosing environment of the module 14 | #' scope. 15 | #' @param class (character) the module can have a class attribute for 16 | #' consistency. If you rely on S3 dispatch, e.g. to override the default print 17 | #' method, you should set this value explicitly. 18 | #' 19 | #' @examples 20 | #' Constructor <- function(dependency) { 21 | #' amodule({ 22 | #' fun <- function(...) dependency(...) 23 | #' }) 24 | #' } 25 | #' instance <- Constructor(identity) 26 | #' instance$fun(1) 27 | #' 28 | #' @export 29 | amodule <- function(expr = {}, 30 | envir = parent.frame(), enclos = baseenv(), 31 | class = NULL) { 32 | mc <- match.call() 33 | mc[[1]] <- quote(modules::module) 34 | mc$class <- NULL 35 | mc$topEncl <- quote(topEncl) 36 | mc$envir <- quote(envir) 37 | topEncl <- list2env(as.list(envir), parent = enclos) 38 | obj <- eval(mc) 39 | class(obj) <- c(class, class(obj)) 40 | obj 41 | } 42 | -------------------------------------------------------------------------------- /R/base-override.R: -------------------------------------------------------------------------------- 1 | # We mask some base functions within modules to signal the user potential 2 | # problems. Problematic are 3 | # - use and library: they change the global state of an R session and not the 4 | # search path of a module 5 | # - source: most likely is used when 'use' would be a better choice 6 | 7 | llibrary <- function(...) { 8 | # l(ocal)library 9 | warning(paste0( 10 | "Packages loaded with 'library' may not be available inside a module. ", 11 | "For loading packages in a module, use 'import' instead.")) 12 | mc <- match.call() 13 | mc[[1]] <- quote(base::library) 14 | eval(mc, envir = parent.frame()) 15 | } 16 | 17 | lattach <- function(...) { 18 | # l(ocal)attach 19 | warning(paste0( 20 | "Objects, including modules, loaded with 'attach' may not be available ", 21 | "inside a module. To attach an object to the search path, use 'use' with ", 22 | "'attach = TRUE' instead.")) 23 | mc <- match.call() 24 | mc[[1]] <- quote(base::attach) 25 | eval(mc, envir = parent.frame()) 26 | } 27 | 28 | lsource <- function(...) { 29 | # l(ocal)source 30 | message(paste0( 31 | "Using 'source' inside a module often can be replaced by 'use' or 'expose'. ", 32 | "Consider the examples in the 'scripts as modules' section in the vignette. ", 33 | "Deactivate this message with 'suppressMessages'." 34 | )) 35 | mc <- match.call() 36 | mc[[1]] <- quote(base::source) 37 | eval(mc, envir = parent.frame()) 38 | } 39 | -------------------------------------------------------------------------------- /R/class.R: -------------------------------------------------------------------------------- 1 | class <- function(x, of) { 2 | if (missing(of)) base::class(x) 3 | else { class(x) <- c(of, base::class(x)); x } 4 | } 5 | -------------------------------------------------------------------------------- /R/depend.R: -------------------------------------------------------------------------------- 1 | #' Declare dependencies of modules 2 | #' 3 | #' This function will check for a dependency and tries to make it available 4 | #' if it is not. This is a generic function. Currently only a default method 5 | #' exists which assumes a package name as argument. If a package is not 6 | #' installed \code{depend} tries to install it. 7 | #' 8 | #' @param on (character) a package name 9 | #' @param version (character) a version, defaults to 'any' 10 | #' @param libPath (character | NULL) a path to the library (folder where 11 | #' packages are installed) 12 | #' @param ... arguments passed to \link{install.packages} 13 | #' 14 | #' @return 15 | #' \code{TRUE} if dependency is available or successfully installed. An error if 16 | #' dependency can not be installed and is not available. 17 | #' 18 | #' @export 19 | #' @examples 20 | #' # Depend on certain R version 21 | #' depend("base", "3.0.0") 22 | #' # Depend on package version 23 | #' depend("modules", "0.6.0") 24 | depend <- function(on, ...) UseMethod("depend") 25 | 26 | #' @rdname depend 27 | #' @export 28 | depend.default <- function(on, version = "any", libPath = NULL, ...) { 29 | stopifnot(length(on) == 1 && is.character(on)) 30 | stopifnot(is.character(version)) 31 | stopifnot(is.null(libPath) || is.character(libPath)) 32 | 33 | needsUpdate <- function(on) { 34 | if (!is.element(on, lib())) { 35 | TRUE 36 | } else if (version == "any") { 37 | FALSE 38 | } else if (pkgVersion(on) < version) { 39 | TRUE 40 | } else { 41 | FALSE 42 | } 43 | } 44 | 45 | lib <- function() { 46 | installed.packages(lib.loc = libPath)[, "Package", drop = TRUE] 47 | } 48 | 49 | pkgVersion <- function(on) { 50 | packageVersion(on, libPath) 51 | } 52 | 53 | if (needsUpdate(on)) install.packages(on, lib = libPath, ...) 54 | if (needsUpdate(on)) { # check if we now have the correct version 55 | stop("'", on, "' package installation failed for version ", version) 56 | } 57 | 58 | invisible(TRUE) 59 | } 60 | -------------------------------------------------------------------------------- /R/export.R: -------------------------------------------------------------------------------- 1 | #' Export mechanism for modules 2 | #' 3 | #' You can declare exports very much like the export mechanism in R packages: 4 | #' you define which objects from the module you make available to a user. All 5 | #' other objects are kept private, local, to the module. 6 | #' 7 | #' @param ... (character, or unquoted expression) names to export from module. A 8 | #' character of length 1 with a leading "^" is interpreted as regular 9 | #' expression. Arguments can be named and used for renaming exports. 10 | #' @param where (environment) typically the calling environment. Should only be 11 | #' relevant for testing. 12 | #' 13 | #' @details A module can have several export declarations, e.g. directly in 14 | #' front of each function definition. That means: exports stack up. When you 15 | #' supply a regular expression, however, only one export pattern should be 16 | #' declared. A regular expression is denoted, as a convention, as character 17 | #' vector of length one with a leading "^". 18 | #' 19 | #' When \code{export} is called outside of a module, it has no effect and 20 | #' returns early. A warning is raised in this case. 21 | #' 22 | #' @examples 23 | #' module({ 24 | #' export("foo") 25 | #' foo <- function() "foo" 26 | #' bar <- function() "bar" 27 | #' }) 28 | #' 29 | #' module({ 30 | #' export("foo") 31 | #' foo <- function() "foo" 32 | #' export("bar") 33 | #' bar <- function() "bar" 34 | #' }) 35 | #' 36 | #' module({ 37 | #' export("foo", "bar") 38 | #' foo <- function() "foo" 39 | #' bar <- function() "bar" 40 | #' }) 41 | #' 42 | #' module({ 43 | #' export("^f.*$") 44 | #' foo <- function() "foo" 45 | #' bar <- function() "bar" 46 | #' }) 47 | #' 48 | #' module({ 49 | #' export(bar = foo) 50 | #' foo <- function() "foo" 51 | #' }) 52 | #' @export 53 | export <- function(..., where = parent.frame()) { 54 | if (exportCalledOutsideOfModule(where)) return(invisible(NULL)) 55 | exportWarnOnNonStandardCalls(match.call()) 56 | objectsToExport <- deparseEllipsis(match.call(), "where") 57 | currentExports <- exportGetCurrentValue(where) 58 | currentExports <- currentExports[currentExports != "^*"] 59 | assign( 60 | exportNameWithinModule(), 61 | c(currentExports, objectsToExport), 62 | envir = where 63 | ) 64 | invisible(NULL) 65 | } 66 | 67 | exportCalledOutsideOfModule <- function(where) { 68 | calledOutsideOfModule <- !exists(exportNameWithinModule(), where, inherits = FALSE) 69 | if (calledOutsideOfModule) { 70 | warning("Calling 'export' outside of a module has no effect.") 71 | } 72 | calledOutsideOfModule 73 | } 74 | 75 | exportWarnOnNonStandardCalls <- function(call) { 76 | # exporting with do.call is not working properly, so we throw a warning, in 77 | # case we can detect it. Consider the following examples: 78 | # m <- module({ 79 | # sm <- module({ 80 | # x <- 1 81 | # fun <- function() x 82 | # }) 83 | # do.call(export, list(fun = sm$fun)) 84 | # }) 85 | # It will not work, although `export(fun = sm$fun)` does work as expected. 86 | # This is extremely difficult to debug and it seems to be better to turn it 87 | # off until someone can fix it. 88 | if (length(deparse(call[[1]])) > 1) { 89 | warning( 90 | "Detected a non standard call to export. The export function relies heavily ", 91 | "on non standard evaluation and may not work as expected combined with 'do.call' ", 92 | "or 'lapply'. See the docs and https://github.com/wahani/modules/issues/19 for ", 93 | "a discussion." 94 | ) 95 | } 96 | } 97 | 98 | exportNameWithinModule <- function() ".__exports__" 99 | 100 | exportGetCurrentValue <- function(envir) { 101 | get(exportNameWithinModule(), envir = envir) 102 | } 103 | 104 | exportExtract2List <- function(envir) { 105 | exports <- exportResolveFinalValue(envir) 106 | objectsAndNames <- Map(exportExtractElement(envir), exports, names(exports)) 107 | module <- lapply(objectsAndNames, function(x) x$object) 108 | names(module) <- vapply(objectsAndNames, function(x) x$name, character(1)) 109 | duplicateNames <- names(module)[duplicated(names(module))] 110 | if (length(duplicateNames) > 0) warning("Found duplicate names in exports!") 111 | module 112 | } 113 | 114 | exportResolveFinalValue <- function(envir) { 115 | isRegEx <- function(s) length(s) == 1 && grepl("^\\^", s) 116 | exports <- exportGetCurrentValue(envir) 117 | if (isRegEx(exports)) exports <- ls(envir, pattern = exports) 118 | if (is.null(names(exports))) names(exports) <- rep("", length(exports)) 119 | exports 120 | } 121 | 122 | exportExtractElement <- function(where) { 123 | function(element, name) { 124 | name <- if (name == "") element else name 125 | # we need to make sure that special names, 126 | # - infix operators: %*%, 127 | # - S3 methods for binary operators: ==.foo 128 | # - names with whitespace 129 | # - single character punctuation: ! 130 | # are parsed correctly 131 | regexp <- "^%.*%$|^[[:alnum:][:space:]]+$|^[[:punct:]]{2,}.*$|^[[:punct:]]$" 132 | element <- if (grepl(regexp, element)) paste0("`", element, "`") else element # Exclude Linting 133 | object <- tryCatch( 134 | eval(parse(text = element), where, baseenv()), 135 | error = function(e) { 136 | stop( 137 | call. = FALSE, 138 | sprintf("unable to resolve export: %s\nfailed with\n%s", name, e) 139 | ) 140 | } 141 | ) 142 | list(name = name, object = object) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /R/expose.R: -------------------------------------------------------------------------------- 1 | #' Expose module contents 2 | #' 3 | #' Use \code{expose} to copy the exported member of a module to the calling 4 | #' environment. This is useful for a simple reexport of member functions and 5 | #' generally for object composition. 6 | #' 7 | #' @param module (character | module) a module as file or folder name or a list 8 | #' representing a module. 9 | #' @param ... (character, or unquoted expression) elements to be exposed. 10 | #' Defaults to all. 11 | #' @param reInit (logical) whether to re-initialize module. This is only 12 | #' relevant if a module has \emph{state} which can be changed. This argument 13 | #' is passed to \link{as.module}. 14 | #' @param where (environment) typically the calling environment. Should only be 15 | #' relevant for testing. 16 | #' 17 | #' @details You call this function for its side effects. It is a variation of 18 | #' \link{use} where instead of returning a module as return value, the 19 | #' elements are copied to the calling environment. 20 | #' 21 | #' @export 22 | #' @examples 23 | #' m1 <- module({ 24 | #' foo <- function() "foo" 25 | #' }) 26 | #' m2 <- module({ 27 | #' bar <- function() "bar" 28 | #' }) 29 | #' # Now we create a module with 'foo' and 'bar' as member functions. 30 | #' m3 <- module({ 31 | #' expose(m1) 32 | #' expose(m2) 33 | #' }) 34 | #' m3$foo() 35 | #' m3$bar() 36 | expose <- function(module, ..., reInit = TRUE, where = parent.frame()) { 37 | mc <- match.call(expand.dots = TRUE) 38 | mc[[1]] <- quote(modules::use) 39 | module <- eval(mc, where) 40 | makeAssignment(module, names(module), where) 41 | invisible(NULL) 42 | } 43 | -------------------------------------------------------------------------------- /R/extend.R: -------------------------------------------------------------------------------- 1 | #' Extend existing module definitions 2 | #' 3 | #' \link{extend} can be used to extend an existing module definition. This can 4 | #' be very useful to write unit tests when they need to have access to private 5 | #' member functions of the module. This function breaks encapsulation of modules 6 | #' and should be used with great care. As a mechanism for reuse consider 7 | #' 'composition' using \link{expose} and \link{use}. 8 | #' 9 | #' @param module (character | module) a module as file or folder name or a list 10 | #' representing a module. 11 | #' @param with (expression) an expression to add to the module definition. 12 | #' 13 | #' @details 14 | #' A module can be characterized by its source code, the top enclosing 15 | #' environment and the environment the module has been defined in. 16 | #' \link{extend} will keep the latter two intact and only change the source 17 | #' code. That means that the new module will have the same scope as the module 18 | #' to be extended. \link{import}, \link{use}, and \link{export} declarations 19 | #' can be added as needed. 20 | #' 21 | #' This approach gives access to all implementation details of a module and 22 | #' breaks encapsulation. Possible use cases are: unit tests, and hacking the 23 | #' module system when necessary. For general reuse of modules, consider using 24 | #' \link{expose} and \link{use} which are safer to use. 25 | #' 26 | #' Since \code{extend} will alter the source code, the state of the 27 | #' module is ignored and will not be present in the new module. A fresh 28 | #' instance of that new module is returned and can in turn be extended and/or 29 | #' treated like any other module. 30 | #' 31 | #' @export 32 | #' @examples 33 | #' m1 <- module({ 34 | #' foo <- function() "foo" 35 | #' }) 36 | #' m2 <- extend(m1, { 37 | #' bar <- function() "bar" 38 | #' }) 39 | #' m1$foo() 40 | #' m2$foo() 41 | #' m2$bar() 42 | #' # For unit tests consider using: 43 | #' extend(m1, { 44 | #' stopifnot(foo() == "foo") 45 | #' }) 46 | extend <- function(module, with) { 47 | extendCheckPrerequisites(module) 48 | originalExpr <- attr(module, "expr") 49 | additionalExpr <- match.call()$with 50 | newExpr <- c(expression(), originalExpr, additionalExpr) 51 | ModuleConst(newExpr, attr(module, "topEncl"), attr(module, "topenv")) 52 | } 53 | 54 | extendCheckPrerequisites <- function(module) { 55 | hasAttributes <- c("expr", "topEncl") %in% names(attributes(module)) 56 | stopifnot(all(hasAttributes)) 57 | } 58 | -------------------------------------------------------------------------------- /R/getSearchPath.R: -------------------------------------------------------------------------------- 1 | #' Get the search path of an environment 2 | #' 3 | #' Returns a list with the environments or names of the environments on the 4 | #' search path. These functions are used for testing, use \link{search} instead. 5 | #' 6 | #' @param where (environment | module | function) the object for the search path 7 | #' should be investigated. If we supply a list with functions (e.g. a module), 8 | #' the environment of the first function in that list is used. 9 | #' 10 | #' @export 11 | #' @rdname utilityFunctions 12 | #' 13 | #' @examples 14 | #' getSearchPath() 15 | #' getSearchPathNames() 16 | #' getSearchPathContent() 17 | #' 18 | #' m <- module({ 19 | #' export("foo") 20 | #' import("stats", "median") 21 | #' foo <- function() "foo" 22 | #' bar <- function() "bar" 23 | #' }) 24 | #' 25 | #' getSearchPathContent(m) 26 | #' 27 | getSearchPath <- function(where = parent.frame()) { 28 | if (is.function(where)) where <- environment(where) 29 | if (listWithFunction(where)) where <- environment(getFirstFunction(where)) 30 | stopifnot(is.environment(where)) 31 | if (identical(where, emptyenv())) list(where) 32 | else c(where, Recall(parent.env(where))) 33 | } 34 | 35 | #' @export 36 | #' @rdname utilityFunctions 37 | getSearchPathNames <- function(where = parent.frame()) { 38 | vapply(getSearchPath(where), environmentName, character(1)) 39 | } 40 | 41 | #' @export 42 | #' @rdname utilityFunctions 43 | getSearchPathContent <- function(where = parent.frame()) { 44 | out <- lapply(getSearchPath(where), function(x) ls(envir = x)) 45 | names(out) <- getSearchPathNames(where) 46 | class(out, c("SearchPathContent")) 47 | } 48 | 49 | #' @export 50 | print.SearchPathContent <- function(x, ...) { 51 | str(x) 52 | } 53 | 54 | listWithFunction <- function(x) { 55 | if (!is.list(x)) return(FALSE) 56 | any(vapply(x, is.function, logical(1))) 57 | } 58 | 59 | getFirstFunction <- function(x) { 60 | Filter(is.function, x)[[1]] 61 | } 62 | 63 | #' @export 64 | #' @rdname utilityFunctions 65 | getSearchPathDuplicates <- function(where = parent.frame()) { 66 | sp <- getSearchPathContent(where) 67 | localNames <- sp[[1]] 68 | spNames <- unlist(sp[-1]) 69 | duplicates <- localNames[is.element(localNames, spNames)] 70 | out <- lapply(duplicates, function(d) { 71 | names(sp[-1])[unlist(lapply(sp[-1], is.element, el = d))] 72 | }) 73 | names(out) <- duplicates 74 | out 75 | } 76 | -------------------------------------------------------------------------------- /R/import.R: -------------------------------------------------------------------------------- 1 | #' Import mechanism for modules 2 | #' 3 | #' You can declare imports similar to what we would do in a R package: we list 4 | #' complete packages or single function names from a package. These listed 5 | #' imports are made available inside the module scope. 6 | #' 7 | #' @param from (character, or unquoted expression) a package name 8 | #' @param ... (character, or unquoted expression) names to import from package. 9 | #' @param where (environment) typically the calling environment. Should only be 10 | #' relevant for testing. 11 | #' @param attach (logical) whether to attach the imports to the search path. 12 | #' @param except (character | NULL) a character vactor excluding any packages 13 | #' from being imported. 14 | #' 15 | #' @details 16 | #' \code{import} and \link{use} can replace \link{library} and \link{attach}. 17 | #' However they behave differently and are only designed to be used within 18 | #' modules. Both will work when called in the \code{.GlobalEnv} but here they 19 | #' should only be used for development and debugging of modules. 20 | #' 21 | #' \code{import} adds a layer to a local search path. More precisely to the 22 | #' calling environment, which is the environment supplied by \code{where}. 23 | #' It will alter the state of the calling environment. This is very 24 | #' similar to how the \link{library} function and the \link{search} path are 25 | #' constructed in base R. Noticeable differences are that we can choose to 26 | #' only import particular functions instead of complete packages. Further we 27 | #' do not have to mutate the calling environment by setting attach to 28 | #' \code{FALSE}. Regardless of the \code{attach} argument, \code{import} will 29 | #' return an environment with the imports and can be bound to a name. 30 | #' \link{library} will also load packages in the 'Depends' field of a package, 31 | #' this is something \code{import} will not do. 32 | #' 33 | #' Only one \code{import} declaration per package is allowed. A second call to 34 | #' import will remove the previous one from the search path. Then the new 35 | #' import layer is added. If several smaller import declarations are 36 | #' desirable, use \code{attach = FALSE} and bind the return value of 37 | #' \code{import} to a name. 38 | #' 39 | #' @return An \link{environment} is returned invisibly comprising the imports. 40 | #' 41 | #' @export 42 | #' @examples 43 | #' m <- module({ 44 | #' # Single object from package 45 | #' import("stats", "median") 46 | #' # Complete package 47 | #' import("stats") 48 | #' # Without side-effects 49 | #' stats <- import("stats", attach = FALSE) 50 | #' median <- function(x) stats$median(x) 51 | #' }) 52 | import <- function(from, ..., attach = TRUE, where = parent.frame()) { 53 | where <- importCheckAttach(where, attach) 54 | pkg <- importGetPkgName(match.call()) 55 | objectsToImport <- importGetSelection(match.call(), pkg) 56 | addDependency(pkg, objectsToImport, where, makeDelayedAssignment, pkg) 57 | invisible(parent.env(where)) 58 | } 59 | 60 | #' @export 61 | #' @rdname import 62 | importDefaultPackages <- function(except = NULL, where = parent.frame()) { 63 | pkgs <- getOption( 64 | "defaultPackages", 65 | c("datasets", "utils", "grDevices", "graphics", "stats", "methods")) 66 | pkgs <- setdiff(pkgs, except) 67 | if ("methods" %in% pkgs) pkgs <- unique(c("methods", pkgs)) 68 | for (pkg in pkgs) do.call(modules::import, list(from = pkg), envir = where) 69 | } 70 | 71 | importCheckAttach <- function(where, attach) { 72 | if (!attach) new.env(parent = baseenv()) else where 73 | } 74 | 75 | importGetPkgName <- function(mc) { 76 | pkg <- Map(deparse, mc)$from 77 | pkg <- deleteQuotes(pkg) 78 | importCheckInstall(pkg) 79 | } 80 | 81 | importCheckInstall <- function(pkg) { 82 | ind <- !is.element(pkg, installed.packages()[, "Package"]) 83 | if (ind) stop( 84 | "'package:", pkg, "' is not installed!" 85 | ) else pkg 86 | } 87 | 88 | importGetSelection <- function(mc, pkg) { 89 | objectsToImport <- importDeparseEllipses(mc) 90 | if (length(objectsToImport) == 0) importGetNamespaceExports(pkg) 91 | else objectsToImport 92 | } 93 | 94 | importDeparseEllipses <- function(mc) { 95 | args <- Map(deparse, mc) 96 | args[[1]] <- NULL 97 | args$from <- NULL 98 | args$where <- NULL 99 | args$attach <- NULL 100 | args <- unlist(args) 101 | deleteQuotes(args) 102 | } 103 | 104 | importGetNamespaceExports <- function(pkg) { 105 | nsExports <- getNamespaceExports(pkg) 106 | nsDatasets <- data(package = pkg) 107 | nsDatasets <- nsDatasets$results[, "Item"] 108 | nsDatasets <- gsub(" .*", "", nsDatasets) 109 | c(nsExports, nsDatasets) 110 | } 111 | -------------------------------------------------------------------------------- /R/module-class.R: -------------------------------------------------------------------------------- 1 | ModuleParent <- function(parent = baseenv()) { 2 | obj <- new.env(parent = parent) 3 | attr(obj, "name") <- "modules:internals" 4 | # The following objects are made available by default. They can be masked by 5 | # other imports. 6 | makeDelayedAssignment("modules", "depend", into = obj) 7 | makeDelayedAssignment("modules", "export", into = obj) 8 | makeDelayedAssignment("modules", "expose", into = obj) 9 | makeDelayedAssignment("modules", "import", into = obj) 10 | makeDelayedAssignment("modules", "importDefaultPackages", into = obj) 11 | assign("attach", lattach, envir = obj) 12 | assign("library", llibrary, envir = obj) 13 | assign("source", lsource, envir = obj) 14 | makeDelayedAssignment("modules", "module", into = obj) 15 | makeDelayedAssignment("modules", "use", into = obj) 16 | obj 17 | } 18 | 19 | ModuleScope <- function(parent = ModuleParent(), topenv) { 20 | # This is the type to wrap a module. It is the enclosing env of all funs in a 21 | # module 22 | obj <- new.env(parent = parent) 23 | attr(obj, "name") <- "modules:root" 24 | # Here are also the flags. Because of imports it might be hard to find the 25 | # original name-value for the exports. 26 | assign(exportNameWithinModule(), "^*", envir = obj) 27 | assign(useTopenvNameWithinModule(), topenv, envir = obj) 28 | obj 29 | } 30 | 31 | ModuleConst <- function(expr, topEncl, topenv) { 32 | 33 | evalInModule <- function(module, code) { 34 | eval(code, envir = as.environment(module), enclos = emptyenv()) 35 | module 36 | } 37 | 38 | addMetaData <- function(module) { 39 | # This adds attributes to give each new module the necessary 40 | # information to construct a sibling 41 | attr(module, "expr") <- expr 42 | attr(module, "topEncl") <- topEncl 43 | attr(module, "topenv") <- topenv 44 | module 45 | } 46 | 47 | module <- ModuleScope(parent = ModuleParent(topEncl), topenv = topenv) 48 | module <- evalInModule(module, expr) 49 | module <- exportExtract2List(module) 50 | module <- class(module, "module") 51 | addMetaData(module) 52 | 53 | } 54 | -------------------------------------------------------------------------------- /R/module-coercion.R: -------------------------------------------------------------------------------- 1 | #' Coercion for Modules 2 | #' 3 | #' Interfaces to and from modules. 4 | #' 5 | #' @param x something which can be coerced into a module. \code{character} are 6 | #' interpreted as file / folder names. 7 | #' @param ... arguments passed to \link{parse} 8 | #' @param reInit (logical) if a module should be re-initialized 9 | #' @inheritParams module 10 | #' 11 | #' @export 12 | #' @rdname modulecoerce 13 | #' 14 | #' @examples 15 | #' # as.module is used by 'use' so see the vignette for examples: 16 | #' \dontrun{ 17 | #' vignette("modulesInR", "modules") 18 | #' } 19 | as.module <- function(x, ...) { 20 | UseMethod("as.module") 21 | } 22 | 23 | #' @export 24 | as.module.default <- function(x, ...) { 25 | as.list(x) 26 | } 27 | 28 | #' @export 29 | as.module.function <- function(x, ...) { 30 | x 31 | } 32 | 33 | 34 | #' @export 35 | #' @rdname modulecoerce 36 | as.module.character <- function(x, topEncl = baseenv(), reInit = TRUE, ..., 37 | envir = parent.frame()) { 38 | stopifnot(length(x) == 1) 39 | 40 | dirAsModule <- function(x, topEncl, ...) { 41 | files <- list.files(x, "\\.(r|R)$", FALSE, TRUE, TRUE) 42 | modules <- lapply(files, fileAsModule, topEncl, ...) 43 | names(modules) <- gsub("\\.(r|R)$", "", sapply(files, basename)) 44 | modules 45 | } 46 | 47 | fileAsModule <- function(x, topEncl, ...) { 48 | do.call(module, list(parse(x, ...), topEncl, envir)) 49 | } 50 | 51 | urlAsModule <- function(x, topEncl, ...) { 52 | download.file(x, fileName <- tempfile(fileext = ".R")) 53 | fileAsModule(fileName, topEncl, ...) 54 | } 55 | 56 | is.url <- function(x) grepl("^(https?|ftp)://", x) 57 | 58 | if (dir.exists(x)) dirAsModule(x, topEncl, ...) 59 | else if (file.exists(x)) fileAsModule(x, topEncl, ...) 60 | else if (is.url(x)) urlAsModule(x, topEncl, ...) 61 | else stop("Can`t find ", x) 62 | 63 | } 64 | 65 | #' @export 66 | #' @rdname modulecoerce 67 | as.module.module <- function(x, reInit = TRUE, ...) { 68 | if (reInit) ModuleConst(attr(x, "expr"), attr(x, "topEncl"), attr(x, "topenv")) 69 | else x 70 | } 71 | -------------------------------------------------------------------------------- /R/module-helper.R: -------------------------------------------------------------------------------- 1 | # helper: 2 | 3 | deparseEllipsis <- function(mc, exclude) { 4 | args <- Map(deparse, mc) 5 | args[[1]] <- NULL 6 | args[exclude] <- NULL 7 | args <- lapply(args, paste0, collapse = "\n") 8 | args <- unlist(args) 9 | deleteQuotes(args) 10 | } 11 | 12 | deleteQuotes <- function(x) { 13 | res <- vapply( 14 | x, FUN.VALUE = character(1), 15 | function(e) { 16 | if (grepl("^[\\\"\\\'].*[\\\"\\\']$", e)) gsub("\\\"|\\\'", "", e) 17 | else e 18 | }) 19 | if (is.null(names(x))) names(res) <- NULL 20 | res 21 | } 22 | 23 | addDependency <- function(from, what, where, assignFun, name) { 24 | # add new dependencies to an existing search path 25 | # 26 | # from (list | env | pkg) a collection which is subset-able with [ 27 | # what (character) names of values in from 28 | # where (environment) where the search path begins 29 | # assignFun (function) how to put 'from::what' 'into' 30 | # name (character) the name on the search path 31 | 32 | addPrefix <- function(name) paste0("modules:", name) 33 | 34 | addDependencyLayer <- function(where, name) { 35 | parentOfWhere <- parent.env(where) 36 | newParent <- new.env(parent = parentOfWhere) 37 | attr(newParent, "name") <- addPrefix(name) 38 | parent.env(where) <- newParent 39 | newParent 40 | } 41 | 42 | cleanSearchPath <- function(where, name) { 43 | sp <- getSearchPath(where) 44 | pos <- Position( 45 | function(el) identical(el, addPrefix(name)), 46 | lapply(sp, attr, "name") 47 | ) 48 | if (is.na(pos)) return(NULL) # stop here 49 | else { 50 | packageStartupMessage( 51 | "Replacing attached import/use on search path for: ", 52 | addPrefix(name), ".") 53 | if (identical(globalenv(), where)) { 54 | detach(pos = pos) 55 | } else { 56 | if (pos == 1) parent.env(where) <- sp[[2]] 57 | else parent.env(sp[[pos - 1]]) <- sp[[pos + 1]] 58 | } 59 | } 60 | } 61 | 62 | messageDuplicates <- function(into) { 63 | duplicates <- getSearchPathDuplicates(into) 64 | if (length(duplicates) == 0) return(NULL) 65 | msg <- sprintf( 66 | "Masking (%s):\n%s", 67 | environmentName(into), 68 | paste(collapse = "\n", paste0( 69 | " `", names(duplicates), "` ", 70 | "from: ", unlist(lapply(duplicates, paste, collapse = ", ")) 71 | )) 72 | ) 73 | packageStartupMessage(msg) 74 | } 75 | 76 | cleanSearchPath(where, name) 77 | # into is a reference to the (new) parent of where: 78 | into <- addDependencyLayer(where, name) 79 | res <- assignFun(from, what, into) 80 | messageDuplicates(into) 81 | res 82 | 83 | } 84 | 85 | makeDelayedAssignment <- function(from, what, into) { 86 | # from: a package name 87 | # what: a character vector 88 | # into: an env 89 | lapply(what, function(x) { 90 | delayedAssign( 91 | x, 92 | value = getExportedValue(from, x), 93 | assign.env = as.environment(into) 94 | ) 95 | }) 96 | } 97 | 98 | makeAssignment <- function(from, what, into) { 99 | # from: a list of values 100 | # what: a character vector 101 | # into: an env 102 | mapply(assign, what, from[what], MoreArgs = list(envir = into)) 103 | } 104 | -------------------------------------------------------------------------------- /R/module.R: -------------------------------------------------------------------------------- 1 | #' Define Modules in R 2 | #' 3 | #' Use \code{module} to define self contained organisational units. Modules have 4 | #' their own search path. \link{import} can be used to import packages. 5 | #' \link{use} can be used to import other modules. Use \link{export} to define 6 | #' which objects to export. \link{expose} can be used to reuse function 7 | #' definitions from another module. 8 | #' 9 | #' @param expr an expression 10 | #' @param topEncl (environment) the root of the local search path. It is tried 11 | #' to find a good default via \link{autoTopEncl}. 12 | #' @param envir,where (environment) the environment from where \code{module} is 13 | #' called. Used to determine the top level environment and should not be 14 | #' supplied by the use. 15 | #' 16 | #' @details 17 | #' \code{topEncl} is the environment where the search of the module ends. 18 | #' \code{autoTopEncl} handles the different situations. In general it defaults 19 | #' to the base environment or the environment from which \code{module} has 20 | #' been called. If you are using \code{use} or \code{expose} referring to a 21 | #' module in a file, it will always be the base environment. When 22 | #' \code{identical(topenv(parent.frame()), globalenv())} is false it (most 23 | #' likely) means that the module is part of a package. In that case the module 24 | #' defines a sub unit within a package but has access to the packages 25 | #' namespace. This is relevant when you use the function module explicitly. 26 | #' When you define a nested module the search path connects to the environment 27 | #' of the enclosing module. 28 | #' 29 | #' The use of \link{library}, \link{attach}, and \link{source} are discouraged 30 | #' within modules. They change the global state of an R session, the 31 | #' \link{.GlobalEnv}, and may not have the intended effect within modules. 32 | #' \link{import} and \link{use} can replace calls to \link{library} and 33 | #' \link{attach}. Both will work when called in the \code{.GlobalEnv} 34 | #' but here they should only be used for development and debugging of modules. 35 | #' \link{source} often is used to load additional user code into a session. 36 | #' This is what \link{use} is designed to do within modules. \link{use} will 37 | #' except files and folders to be used. 38 | #' 39 | #' \link{export} will never export a function with a leading "." in its name. 40 | #' 41 | #' \link{expose} is similar to \link{use} but instead of attaching a module it 42 | #' will copy all elements into the calling environment. This means that 43 | #' \emph{exposed} functions can be (re-)exported. 44 | #' 45 | #' \link{extend} can be used to extend an existing module definition. This 46 | #' feature is meant to be used by the module author. This can be very useful 47 | #' to write unit tests when they need to have access to private member 48 | #' functions of the module. It is not safe as a user of a module to use this 49 | #' feature: it breaks encapsulation. When you are looking for mechanisms for 50 | #' reuse \link{expose} and \link{use} should be favoured. 51 | #' 52 | #' 53 | #' @examples 54 | #' \dontrun{ 55 | #' vignette("modulesInR", "modules") 56 | #' } 57 | #' 58 | #' m <- module({ 59 | #' fun <- function(x) x 60 | #' }) 61 | #' 62 | #' m$fun(1) 63 | #' 64 | #' m <- module({ 65 | #' 66 | #' import("stats", "median") 67 | #' export("fun") 68 | #' 69 | #' fun <- function(x) { 70 | #' ## This is an identity function 71 | #' ## x (ANY) 72 | #' x 73 | #' } 74 | #' 75 | #' }) 76 | #' 77 | #' m$fun 78 | #' m 79 | #' 80 | #' @rdname module 81 | #' @export 82 | module <- function(expr = {}, topEncl = autoTopEncl(envir), envir = parent.frame()) { 83 | ModuleConst(match.call()$expr, topEncl, topenv(envir)) 84 | } 85 | 86 | #' @export 87 | print.module <- function(x, ...) { 88 | 89 | getFormals <- function(fun) { 90 | formalsOfFun <- formals(fun) 91 | formalsOfFun[sapply(formalsOfFun, is.character)] <- 92 | lapply(formalsOfFun[sapply(formalsOfFun, is.character)], function(el) { 93 | paste0("\"", el, "\"") 94 | }) 95 | args <- ifelse( 96 | as.character(formalsOfFun) == "", 97 | names(formalsOfFun), 98 | paste(names(formalsOfFun), formalsOfFun, sep = " = ") 99 | ) 100 | paste0("function(", paste(args, collapse = ", "), ")") 101 | } 102 | 103 | getDoc <- function(fun) { 104 | sourceOfFun <- trimws(attr(fun, "srcref")) 105 | sourceOfFun <- sourceOfFun[grep("^##", sourceOfFun)] 106 | paste(sourceOfFun, collapse = "\n") 107 | } 108 | 109 | catFuns <- function(funs) { 110 | for (i in seq_along(funs)) { 111 | cat(names(funs)[i], ":\n", sep = "") 112 | cat(getFormals(funs[[i]]), "\n", sep = "") 113 | docString <- getDoc(funs[[i]]) 114 | if (length(docString) > 0) cat(docString, "\n", sep = "") 115 | cat("\n", sep = "") 116 | } 117 | } 118 | 119 | catRemaining <- function(remaining) { 120 | for (i in seq_along(remaining)) { 121 | cat(paste0(names(remaining)[i], ":\n")) 122 | cat(str(remaining[[i]])) 123 | cat("\n") 124 | } 125 | } 126 | 127 | ind <- vapply(x, is.function, logical(1)) 128 | catFuns(x[ind]) 129 | catRemaining(x[!ind]) 130 | 131 | invisible(x) 132 | 133 | } 134 | 135 | #' @export 136 | #' @rdname module 137 | autoTopEncl <- function(where) { 138 | # if .__exports__ exists I assume it is a nested module: 139 | if (exists(exportNameWithinModule(), where = where)) where 140 | else if (identical(topenv(where), globalenv())) baseenv() 141 | else where 142 | } 143 | -------------------------------------------------------------------------------- /R/testModule.R: -------------------------------------------------------------------------------- 1 | # File with test modules to test scoping inside a package. 2 | 3 | TestModule1 <- function(a) module({ 4 | foo <- identity 5 | b <- a 6 | }) 7 | 8 | TestModule2 <- function() module(topEncl = baseenv(), { 9 | c <- 3 10 | expose(TestModule1(c)) 11 | tm1 <- use(TestModule1)(c) 12 | }) 13 | -------------------------------------------------------------------------------- /R/use.R: -------------------------------------------------------------------------------- 1 | #' Use a module as dependency 2 | #' 3 | #' Use and/or register a module as dependency. The behaviour of use is similar 4 | #' to \link{import} but instead of importing from packages, we import from a 5 | #' module. A module can be defined in a file, or be an object. 6 | #' 7 | #' @param module (character, module) a file or folder name, or an object that 8 | #' can be interpreted as a module: any list-like object would do. 9 | #' @param ... (character, or unquoted expression) names to use from module. 10 | #' @param where (environment) typically the calling environment. Should only be 11 | #' relevant for testing. 12 | #' @param attach (logical) whether to attach the module to the search path. 13 | #' @param reInit (logical) we can use a module as is, or reinitialize it. The 14 | #' default is to reinitialize. This is only relevant should the module be 15 | #' state-full. 16 | #' 17 | #' @details 18 | #' \link{import} and \code{use} can replace \link{library} and \link{attach}. 19 | #' However they behave differently and are only designed to be used within 20 | #' modules. Both will work when called in the \code{.GlobalEnv} but here they 21 | #' should only be used for development and debugging of modules. 22 | #' 23 | #' \code{use} adds a layer to a local search path if \code{attach} is 24 | #' \code{TRUE}. More precisely to the calling environment, which is the 25 | #' environment supplied by \code{where}. Regardless of the \code{attach} 26 | #' argument, \code{use} will return the module invisibly. 27 | #' 28 | #' \code{use} supplies a special mechanism to find the argument \code{module}: 29 | #' generally you can supply a file name or folder name as character. You can 30 | #' also reference objects/names which 'live' outside the module scope. If 31 | #' names are not found within the scope of the module, they are searched for 32 | #' in the environment in which the module has been defined. This happens 33 | #' during initialization of the module, when the \code{use} function is 34 | #' called. 35 | #' 36 | #' Modules can live in files. \code{use} should be used to load them. A module 37 | #' definition in a file does not need to use the \link{module} constructor 38 | #' explicitly. Any R script can be used as the body of a module. 39 | #' 40 | #' When a folder is referenced in \code{use} it is transformed into a list of 41 | #' modules. This is represented as a nested list mimicking the folder 42 | #' structure. Each file in that folder becomes a module. 43 | #' 44 | #' @export 45 | #' @examples 46 | #' m1 <- module({ 47 | #' foo <- function() "foo" 48 | #' }) 49 | #' m2 <- module({ 50 | #' use(m1, attach = TRUE) 51 | #' bar <- function() "bar" 52 | #' m1foo <- function() foo() 53 | #' }) 54 | #' m2$m1foo() 55 | #' m2$bar() 56 | #' 57 | #' \dontrun{ 58 | #' someFile <- tempfile(fileext = ".R") 59 | #' writeLines("foo <- function() 'foo'", someFile) 60 | #' m3 <- use(someFile) 61 | #' m3$foo() 62 | #' otherFile <- tempfile(fileext = ".R") 63 | #' writeLines("bar <- function() 'bar'", otherFile) 64 | #' m4 <- use(otherFile) 65 | #' m4$bar() 66 | #' m5 <- use(tempdir()) 67 | #' m5 68 | #' } 69 | use <- function(module, ..., attach = FALSE, reInit = TRUE, where = parent.frame()) { 70 | 71 | moduleName <- as.character(substitute(module)) 72 | module <- useTryFindModule(module, moduleName, where, match.call()) 73 | name <- if (is.character(module)) module else moduleName 74 | module <- as.module(module, reInit = reInit, envir = where) 75 | module <- useGetSelection(module, match.call(expand.dots = TRUE)) 76 | 77 | if (attach) addDependency( 78 | module, 79 | names(module), 80 | where, 81 | makeAssignment, 82 | name 83 | ) 84 | 85 | invisible(module) 86 | 87 | } 88 | 89 | useTryFindModule <- function(module, moduleName, envir, mc) { 90 | m <- try(module, TRUE) 91 | if (is.error(m)) { 92 | m1 <- try( 93 | eval(mc$module, get(useTopenvNameWithinModule(), envir = envir)), TRUE) 94 | if (is.error(m1)) stop(simpleError(useGetErrorMessage(m), mc)) 95 | else m <- m1 96 | } 97 | m 98 | } 99 | 100 | is.error <- function(x) { 101 | inherits(x, "try-error") 102 | } 103 | 104 | useGetErrorMessage <- function(x) { 105 | attributes(x)$condition$message 106 | } 107 | 108 | useGetSelection <- function(module, mc) { 109 | namesToImport <- deparseEllipsis(mc, c("module", "attach", "reInit", "where")) 110 | if (length(namesToImport) == 0) module 111 | else module[namesToImport] 112 | } 113 | 114 | useTopenvNameWithinModule <- function() { 115 | ".__topenv__" 116 | } 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/wahani/modules/actions/workflows/rcmdcheck.yml/badge.svg?branch=main)](https://github.com/wahani/modules/actions) 2 | [![codecov.io](https://codecov.io/github/wahani/modules/coverage.svg?branch=master)](https://codecov.io/github/wahani/modules?branch=master) 3 | [![CRAN](http://www.r-pkg.org/badges/version/modules)](https://cran.r-project.org/package=modules) 4 | ![Downloads](http://cranlogs.r-pkg.org/badges/modules) 5 | # Modules in R 6 | 7 | Provides modules as an organizational unit for source code. Modules enforce to be more rigorous when defining dependencies and have a local search path. They can be used as a sub unit within packages or in scripts. 8 | 9 | ## Installation 10 | 11 | From CRAN: 12 | 13 | ```r 14 | install.packages("modules") 15 | ``` 16 | 17 | From GitHub: 18 | 19 | 20 | ```r 21 | if (require("devtools")) install_github("wahani/modules") 22 | ``` 23 | 24 | # Introduction 25 | 26 | The key idea of this package is to provide a unit of source code which has it's 27 | own scope. The main and most reliable infrastructure for such organizational 28 | units in the R ecosystem is a package. Modules can be used as stand alone, 29 | ad-hoc substitutes for a package or as a sub-unit within a package. 30 | 31 | When modules are defined inside of packages they act as bags of functions (like 32 | objects as in object-oriented-programming). Outside of packages modules define 33 | entities which only know of the base environment, i.e. within a module the base 34 | environment is the only *package* on the *search path*. Also they are always 35 | represented as a list inside R. 36 | 37 | ## Scoping of modules 38 | 39 | We can create a module using the `modules::module` function. A module is similar 40 | to a function definition; it comprises: 41 | 42 | - the body of the module 43 | - the environment in which it is created (defined implicitly) 44 | - the environment used for the search path, in most cases `baseenv()` (defined 45 | implicitly) 46 | 47 | Similar to a function you may supply arguments to a module; see the vignette on 48 | modules as objects on this topic. 49 | 50 | To illustrate the very basic functionality of a module, consider the following 51 | example: 52 | 53 | 54 | ```r 55 | library("modules") 56 | m <- module({ 57 | foo <- function() "foo" 58 | }) 59 | m$foo() 60 | ``` 61 | 62 | ``` 63 | ## [1] "foo" 64 | ``` 65 | 66 | Here `m` is the collection of objects created inside the module. This is a 67 | `list` with the function `foo` as only element. We can do the same thing and define a module in a separate file: 68 | 69 | **module.R** 70 | 71 | ``` 72 | foo <- function() "foo" 73 | ``` 74 | 75 | **main.R** 76 | 77 | 78 | ```r 79 | m <- modules::use("module.R") 80 | m$foo() 81 | ``` 82 | 83 | ``` 84 | ## [1] "foo" 85 | ``` 86 | 87 | The two examples illustrate the two ways in which modules can be constructed. 88 | Since modules are isolated from the `.GlobalEnv` the following object `x` can 89 | not be found: 90 | 91 | 92 | ```r 93 | x <- "hey" 94 | m <- module({ 95 | someFunction <- function() x 96 | }) 97 | m$someFunction() 98 | ``` 99 | 100 | ``` 101 | ## Error in m$someFunction(): object 'x' not found 102 | ``` 103 | 104 | ```r 105 | getSearchPathContent(m) 106 | ``` 107 | 108 | ``` 109 | ## List of 4 110 | ## $ modules:root : chr "someFunction" 111 | ## $ modules:internals: chr [1:9] "attach" "depend" "export" "expose" ... 112 | ## $ base : chr [1:1244] "-" "-.Date" "-.POSIXt" ":" ... 113 | ## $ R_EmptyEnv : chr(0) 114 | ## - attr(*, "class")= chr [1:2] "SearchPathContent" "list" 115 | ``` 116 | 117 | Two features of modules are important at this point: 118 | 119 | - We can keep the global workspace clean, by introducing a local scope 120 | - We have no direct access to the global environment from modules by default, 121 | enforcing discipline when using any form of dependency (objects and packages) 122 | 123 | The following subsections explain how to work with these two features. 124 | 125 | ## Imports 126 | 127 | If you rely on exported objects of a package you can refer to them explicitly 128 | using `::`: 129 | 130 | 131 | ```r 132 | m <- module({ 133 | functionWithDep <- function(x) stats::median(x) 134 | }) 135 | m$functionWithDep(1:10) 136 | ``` 137 | 138 | ``` 139 | ## [1] 5.5 140 | ``` 141 | 142 | Or you can use `import` for *attaching* single objects or packages. Import acts as a substitute for `library` with an important difference: `library` has the side effect of changing the search path of the complete R session. `import` only changes the search path of the calling environment, i.e. the side effect is local to the module and does not affect the global state of the R session. 143 | 144 | 145 | ```r 146 | m <- module({ 147 | 148 | import("stats", "median") # make median from package stats available 149 | 150 | functionWithDep <- function(x) median(x) 151 | 152 | }) 153 | m$functionWithDep(1:10) 154 | ``` 155 | 156 | ``` 157 | ## [1] 5.5 158 | ``` 159 | 160 | ```r 161 | getSearchPathContent(m) 162 | ``` 163 | 164 | ``` 165 | ## List of 5 166 | ## $ modules:root : chr "functionWithDep" 167 | ## $ modules:stats : chr "median" 168 | ## $ modules:internals: chr [1:9] "attach" "depend" "export" "expose" ... 169 | ## $ base : chr [1:1244] "-" "-.Date" "-.POSIXt" ":" ... 170 | ## $ R_EmptyEnv : chr(0) 171 | ## - attr(*, "class")= chr [1:2] "SearchPathContent" "list" 172 | ``` 173 | 174 | 175 | ```r 176 | m <- module({ 177 | 178 | import("stats") 179 | 180 | functionWithDep <- function(x) median(x) 181 | 182 | }) 183 | m$functionWithDep(1:10) 184 | ``` 185 | 186 | ``` 187 | ## [1] 5.5 188 | ``` 189 | 190 | ## Importing modules 191 | 192 | To *import* other modules, the function `use` can be called. *use* really just means *import module*. With `use` we can load modules: 193 | 194 | - defined in the calling environment of the module definition 195 | - or defined in files or folders (see the corresponding vignette on this topic) 196 | 197 | Consider the following example: 198 | 199 | 200 | ```r 201 | mm <- module({ 202 | m <- use(m) 203 | anotherFunction <- function(x) m$functionWithDep(x) 204 | }) 205 | mm$anotherFunction(1:10) 206 | ``` 207 | 208 | ``` 209 | ## [1] 5.5 210 | ``` 211 | 212 | To load modules from a file we can refer to the file directly: 213 | 214 | 215 | ```r 216 | module({ 217 | m <- use("someFile.R") 218 | # ... 219 | }) 220 | ``` 221 | 222 | ## Exports 223 | 224 | Modules can help to isolate code from the state of the global environment. Now 225 | we may have reduced the complexity in our global environment and moved it into a 226 | module. However, to make it very obvious which parts of a module should be used 227 | we can also define exports. Every non-exported object will not be accessible. 228 | 229 | Properties of exports are: 230 | 231 | - You can list the names of objects in a call to `export`. 232 | - Exports stack up: you can have multiple calls to `export` in a module 233 | definition, i.e. directly in front of each function you want to export. 234 | - Exports can be defined as regular expressions which is indicated by a leading 235 | '^'. In this case only one export declaration should be used. 236 | 237 | 238 | ```r 239 | m <- module({ 240 | 241 | export("fun") 242 | 243 | fun <- identity # public 244 | privateFunction <- identity 245 | 246 | # .named are always private 247 | .privateFunction <- identity 248 | 249 | }) 250 | 251 | m 252 | ``` 253 | 254 | ``` 255 | ## fun: 256 | ## function(x) 257 | ``` 258 | 259 | # Example: Modules as Parallel Process 260 | 261 | One example where you may want to have more control of the enclosing environment 262 | of a function is when you parallelize your code. First consider the case when a 263 | *naive* implementation fails. 264 | 265 | 266 | ```r 267 | library("parallel") 268 | dependency <- identity 269 | fun <- function(x) dependency(x) 270 | 271 | cl <- makeCluster(2) 272 | clusterMap(cl, fun, 1:2) 273 | ``` 274 | 275 | ``` 276 | ## Error in checkForRemoteErrors(val): 2 nodes produced errors; first error: could not find function "dependency" 277 | ``` 278 | 279 | ```r 280 | stopCluster(cl) 281 | ``` 282 | 283 | To make the function `fun` self contained we can define it in a module. 284 | 285 | 286 | ```r 287 | m <- module({ 288 | dependency <- identity 289 | fun <- function(x) dependency(x) 290 | }) 291 | 292 | cl <- makeCluster(2) 293 | clusterMap(cl, m$fun, 1:2) 294 | ``` 295 | 296 | ``` 297 | ## [[1]] 298 | ## [1] 1 299 | ## 300 | ## [[2]] 301 | ## [1] 2 302 | ``` 303 | 304 | ```r 305 | stopCluster(cl) 306 | ``` 307 | 308 | Note that the parallel computing facilities in `R` always provide a way to 309 | handle such situations. Here it is just a matter of organization if you believe 310 | the function itself should handle its dependencies or the parallel interface. 311 | 312 | 313 | # Related Projects 314 | 315 | There exist several projects with similar goals. First of all, the package 316 | [klmr/modules](https://github.com/klmr/modules) aims at providing a unit similar 317 | to what [Python](https://www.python.org/)-modules are. This project is obviously 318 | interesting for you when you have prior knowledge in Python. `klmr/modules` 319 | modules aim for a full replacement of R-packages. Otherwise there is 320 | considerable overlap of features between the two packages. 321 | 322 | Second you may be interested in 323 | [import](https://cran.r-project.org/package=import) which provides convenient 324 | syntax for stating dependencies in script files. This is something which is also 325 | covered here, although, when you are only interested in a replacement for 326 | `library` the package `import` is more focused. 327 | 328 | `modules` in this package can act as objects as in object-orientation. In 329 | contrast to [R6](https://cran.r-project.org/package=R6) and reference classes 330 | implemented in the methods package here these objects are immutable by default. 331 | Furthermore it is not being made easy to change state of a module; but it is not 332 | difficult to do that if you really want to: see the section on coupling below. 333 | Furthermore inheritance is not a feature, instead you have various possibilities 334 | for object composition. 335 | 336 | The development of the `modules` package has been inspired by other languages: 337 | [F#](https://fsharpforfunandprofit.com/posts/organizing-functions/), 338 | [Erlang](https://learnyousomeerlang.com/modules/) and 339 | [julia](https://docs.julialang.org/en/v1/manual/modules/index.html). 340 | 341 | 342 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## Test environments 2 | 3 | * github-action 4 | * win-builder 5 | * local PopOS! 22.04, R 4.3.1 6 | 7 | ## R CMD check results 8 | 9 | * There were no ERRORs or WARNINGs or NOTEs 10 | 11 | There are no notes or issues reported on 12 | 13 | https://cran.r-project.org/web/checks/check_results_modules.html: 14 | 15 | 16 | ## Downstream dependencies 17 | 18 | * To the best of my knowledge there are no problems. 19 | -------------------------------------------------------------------------------- /man/amodule.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/amodule.R 3 | \name{amodule} 4 | \alias{amodule} 5 | \title{Define Augmented and Parameterized Modules} 6 | \usage{ 7 | amodule(expr = { 8 | }, envir = parent.frame(), enclos = baseenv(), class = NULL) 9 | } 10 | \arguments{ 11 | \item{expr}{(expression) a module declaration, same as \link{module}} 12 | 13 | \item{envir}{(environment) environment used to detect 'parameters'} 14 | 15 | \item{enclos}{(environment) the top enclosing environment of the module 16 | scope.} 17 | 18 | \item{class}{(character) the module can have a class attribute for 19 | consistency. If you rely on S3 dispatch, e.g. to override the default print 20 | method, you should set this value explicitly.} 21 | } 22 | \description{ 23 | \code{amodule} is a wrapper around \link{module} and changes the default 24 | environment to which the module connects. In contrast to \code{module} 25 | the top enclosing environment here is always \code{baseenv}. The second 26 | important difference is that the environment in which a module is created has 27 | meaning: all objects are made available to the module scope. This is 28 | what is meant by \emph{augmented} or \emph{parameterized}. Best practice for 29 | the use of this behavior is to return these modules from functions. 30 | } 31 | \examples{ 32 | Constructor <- function(dependency) { 33 | amodule({ 34 | fun <- function(...) dependency(...) 35 | }) 36 | } 37 | instance <- Constructor(identity) 38 | instance$fun(1) 39 | 40 | } 41 | -------------------------------------------------------------------------------- /man/depend.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/depend.R 3 | \name{depend} 4 | \alias{depend} 5 | \alias{depend.default} 6 | \title{Declare dependencies of modules} 7 | \usage{ 8 | depend(on, ...) 9 | 10 | \method{depend}{default}(on, version = "any", libPath = NULL, ...) 11 | } 12 | \arguments{ 13 | \item{on}{(character) a package name} 14 | 15 | \item{...}{arguments passed to \link{install.packages}} 16 | 17 | \item{version}{(character) a version, defaults to 'any'} 18 | 19 | \item{libPath}{(character | NULL) a path to the library (folder where 20 | packages are installed)} 21 | } 22 | \value{ 23 | \code{TRUE} if dependency is available or successfully installed. An error if 24 | dependency can not be installed and is not available. 25 | } 26 | \description{ 27 | This function will check for a dependency and tries to make it available 28 | if it is not. This is a generic function. Currently only a default method 29 | exists which assumes a package name as argument. If a package is not 30 | installed \code{depend} tries to install it. 31 | } 32 | \examples{ 33 | # Depend on certain R version 34 | depend("base", "3.0.0") 35 | # Depend on package version 36 | depend("modules", "0.6.0") 37 | } 38 | -------------------------------------------------------------------------------- /man/export.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/export.R 3 | \name{export} 4 | \alias{export} 5 | \title{Export mechanism for modules} 6 | \usage{ 7 | export(..., where = parent.frame()) 8 | } 9 | \arguments{ 10 | \item{...}{(character, or unquoted expression) names to export from module. A 11 | character of length 1 with a leading "^" is interpreted as regular 12 | expression. Arguments can be named and used for renaming exports.} 13 | 14 | \item{where}{(environment) typically the calling environment. Should only be 15 | relevant for testing.} 16 | } 17 | \description{ 18 | You can declare exports very much like the export mechanism in R packages: 19 | you define which objects from the module you make available to a user. All 20 | other objects are kept private, local, to the module. 21 | } 22 | \details{ 23 | A module can have several export declarations, e.g. directly in 24 | front of each function definition. That means: exports stack up. When you 25 | supply a regular expression, however, only one export pattern should be 26 | declared. A regular expression is denoted, as a convention, as character 27 | vector of length one with a leading "^". 28 | } 29 | \examples{ 30 | module({ 31 | export("foo") 32 | foo <- function() "foo" 33 | bar <- function() "bar" 34 | }) 35 | 36 | module({ 37 | export("foo") 38 | foo <- function() "foo" 39 | export("bar") 40 | bar <- function() "bar" 41 | }) 42 | 43 | module({ 44 | export("foo", "bar") 45 | foo <- function() "foo" 46 | bar <- function() "bar" 47 | }) 48 | 49 | module({ 50 | export("^f.*$") 51 | foo <- function() "foo" 52 | bar <- function() "bar" 53 | }) 54 | 55 | module({ 56 | export(bar = foo) 57 | foo <- function() "foo" 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /man/expose.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/expose.R 3 | \name{expose} 4 | \alias{expose} 5 | \title{Expose module contents} 6 | \usage{ 7 | expose(module, ..., reInit = TRUE, where = parent.frame()) 8 | } 9 | \arguments{ 10 | \item{module}{(character | module) a module as file or folder name or a list 11 | representing a module.} 12 | 13 | \item{...}{(character, or unquoted expression) elements to be exposed. 14 | Defaults to all.} 15 | 16 | \item{reInit}{(logical) whether to re-initialize module. This is only 17 | relevant if a module has \emph{state} which can be changed. This argument 18 | is passed to \link{as.module}.} 19 | 20 | \item{where}{(environment) typically the calling environment. Should only be 21 | relevant for testing.} 22 | } 23 | \description{ 24 | Use \code{expose} to copy the exported member of a module to the calling 25 | environment. This is useful for a simple reexport of member functions and 26 | generally for object composition. 27 | } 28 | \details{ 29 | You call this function for its side effects. It is a variation of 30 | \link{use} where instead of returning a module as return value, the 31 | elements are copied to the calling environment. 32 | } 33 | \examples{ 34 | m1 <- module({ 35 | foo <- function() "foo" 36 | }) 37 | m2 <- module({ 38 | bar <- function() "bar" 39 | }) 40 | # Now we create a module with 'foo' and 'bar' as member functions. 41 | m3 <- module({ 42 | expose(m1) 43 | expose(m2) 44 | }) 45 | m3$foo() 46 | m3$bar() 47 | } 48 | -------------------------------------------------------------------------------- /man/extend.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/extend.R 3 | \name{extend} 4 | \alias{extend} 5 | \title{Extend existing module definitions} 6 | \usage{ 7 | extend(module, with) 8 | } 9 | \arguments{ 10 | \item{module}{(character | module) a module as file or folder name or a list 11 | representing a module.} 12 | 13 | \item{with}{(expression) an expression to add to the module definition.} 14 | } 15 | \description{ 16 | \link{extend} can be used to extend an existing module definition. This can 17 | be very useful to write unit tests when they need to have access to private 18 | member functions of the module. This function breaks encapsulation of modules 19 | and should be used with great care. As a mechanism for reuse consider 20 | 'composition' using \link{expose} and \link{use}. 21 | } 22 | \details{ 23 | A module can be characterized by its source code, the top enclosing 24 | environment and the environment the module has been defined in. 25 | \link{extend} will keep the latter two intact and only change the source 26 | code. That means that the new module will have the same scope as the module 27 | to be extended. \link{import}, \link{use}, and \link{export} declarations 28 | can be added as needed. 29 | 30 | This approach gives access to all implementation details of a module and 31 | breaks encapsulation. Possible use cases are: unit tests, and hacking the 32 | module system when necessary. For general reuse of modules, consider using 33 | \link{expose} and \link{use} which are safer to use. 34 | 35 | Since \code{extend} will alter the source code, the state of the 36 | module is ignored and will not be present in the new module. A fresh 37 | instance of that new module is returned and can in turn be extended and/or 38 | treated like any other module. 39 | } 40 | \examples{ 41 | m1 <- module({ 42 | foo <- function() "foo" 43 | }) 44 | m2 <- extend(m1, { 45 | bar <- function() "bar" 46 | }) 47 | m1$foo() 48 | m2$foo() 49 | m2$bar() 50 | # For unit tests consider using: 51 | extend(m1, { 52 | stopifnot(foo() == "foo") 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /man/import.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/import.R 3 | \name{import} 4 | \alias{import} 5 | \alias{importDefaultPackages} 6 | \title{Import mechanism for modules} 7 | \usage{ 8 | import(from, ..., attach = TRUE, where = parent.frame()) 9 | 10 | importDefaultPackages(except = NULL, where = parent.frame()) 11 | } 12 | \arguments{ 13 | \item{from}{(character, or unquoted expression) a package name} 14 | 15 | \item{...}{(character, or unquoted expression) names to import from package.} 16 | 17 | \item{attach}{(logical) whether to attach the imports to the search path.} 18 | 19 | \item{where}{(environment) typically the calling environment. Should only be 20 | relevant for testing.} 21 | 22 | \item{except}{(character | NULL) a character vactor excluding any packages 23 | from being imported.} 24 | } 25 | \value{ 26 | An \link{environment} is returned invisibly comprising the imports. 27 | } 28 | \description{ 29 | You can declare imports similar to what we would do in a R package: we list 30 | complete packages or single function names from a package. These listed 31 | imports are made available inside the module scope. 32 | } 33 | \details{ 34 | \code{import} and \link{use} can replace \link{library} and \link{attach}. 35 | However they behave differently and are only designed to be used within 36 | modules. Both will work when called in the \code{.GlobalEnv} but here they 37 | should only be used for development and debugging of modules. 38 | 39 | \code{import} adds a layer to a local search path. More precisely to the 40 | calling environment, which is the environment supplied by \code{where}. 41 | It will alter the state of the calling environment. This is very 42 | similar to how the \link{library} function and the \link{search} path are 43 | constructed in base R. Noticeable differences are that we can choose to 44 | only import particular functions instead of complete packages. Further we 45 | do not have to mutate the calling environment by setting attach to 46 | \code{FALSE}. Regardless of the \code{attach} argument, \code{import} will 47 | return an environment with the imports and can be bound to a name. 48 | \link{library} will also load packages in the 'Depends' field of a package, 49 | this is something \code{import} will not do. 50 | 51 | Only one \code{import} declaration per package is allowed. A second call to 52 | import will remove the previous one from the search path. Then the new 53 | import layer is added. If several smaller import declarations are 54 | desirable, use \code{attach = FALSE} and bind the return value of 55 | \code{import} to a name. 56 | } 57 | \examples{ 58 | m <- module({ 59 | # Single object from package 60 | import("stats", "median") 61 | # Complete package 62 | import("stats") 63 | # Without side-effects 64 | stats <- import("stats", attach = FALSE) 65 | median <- function(x) stats$median(x) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /man/module.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/module.R 3 | \name{module} 4 | \alias{module} 5 | \alias{autoTopEncl} 6 | \title{Define Modules in R} 7 | \usage{ 8 | module(expr = { 9 | }, topEncl = autoTopEncl(envir), envir = parent.frame()) 10 | 11 | autoTopEncl(where) 12 | } 13 | \arguments{ 14 | \item{expr}{an expression} 15 | 16 | \item{topEncl}{(environment) the root of the local search path. It is tried 17 | to find a good default via \link{autoTopEncl}.} 18 | 19 | \item{envir, where}{(environment) the environment from where \code{module} is 20 | called. Used to determine the top level environment and should not be 21 | supplied by the use.} 22 | } 23 | \description{ 24 | Use \code{module} to define self contained organisational units. Modules have 25 | their own search path. \link{import} can be used to import packages. 26 | \link{use} can be used to import other modules. Use \link{export} to define 27 | which objects to export. \link{expose} can be used to reuse function 28 | definitions from another module. 29 | } 30 | \details{ 31 | \code{topEncl} is the environment where the search of the module ends. 32 | \code{autoTopEncl} handles the different situations. In general it defaults 33 | to the base environment or the environment from which \code{module} has 34 | been called. If you are using \code{use} or \code{expose} referring to a 35 | module in a file, it will always be the base environment. When 36 | \code{identical(topenv(parent.frame()), globalenv())} is false it (most 37 | likely) means that the module is part of a package. In that case the module 38 | defines a sub unit within a package but has access to the packages 39 | namespace. This is relevant when you use the function module explicitly. 40 | When you define a nested module the search path connects to the environment 41 | of the enclosing module. 42 | 43 | The use of \link{library}, \link{attach}, and \link{source} are discouraged 44 | within modules. They change the global state of an R session, the 45 | \link{.GlobalEnv}, and may not have the intended effect within modules. 46 | \link{import} and \link{use} can replace calls to \link{library} and 47 | \link{attach}. Both will work when called in the \code{.GlobalEnv} 48 | but here they should only be used for development and debugging of modules. 49 | \link{source} often is used to load additional user code into a session. 50 | This is what \link{use} is designed to do within modules. \link{use} will 51 | except files and folders to be used. 52 | 53 | \link{export} will never export a function with a leading "." in its name. 54 | 55 | \link{expose} is similar to \link{use} but instead of attaching a module it 56 | will copy all elements into the calling environment. This means that 57 | \emph{exposed} functions can be (re-)exported. 58 | 59 | \link{extend} can be used to extend an existing module definition. This 60 | feature is meant to be used by the module author. This can be very useful 61 | to write unit tests when they need to have access to private member 62 | functions of the module. It is not safe as a user of a module to use this 63 | feature: it breaks encapsulation. When you are looking for mechanisms for 64 | reuse \link{expose} and \link{use} should be favoured. 65 | } 66 | \examples{ 67 | \dontrun{ 68 | vignette("modulesInR", "modules") 69 | } 70 | 71 | m <- module({ 72 | fun <- function(x) x 73 | }) 74 | 75 | m$fun(1) 76 | 77 | m <- module({ 78 | 79 | import("stats", "median") 80 | export("fun") 81 | 82 | fun <- function(x) { 83 | ## This is an identity function 84 | ## x (ANY) 85 | x 86 | } 87 | 88 | }) 89 | 90 | m$fun 91 | m 92 | 93 | } 94 | -------------------------------------------------------------------------------- /man/modulecoerce.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/module-coercion.R 3 | \name{as.module} 4 | \alias{as.module} 5 | \alias{as.module.character} 6 | \alias{as.module.module} 7 | \title{Coercion for Modules} 8 | \usage{ 9 | as.module(x, ...) 10 | 11 | \method{as.module}{character}(x, topEncl = baseenv(), reInit = TRUE, ..., envir = parent.frame()) 12 | 13 | \method{as.module}{module}(x, reInit = TRUE, ...) 14 | } 15 | \arguments{ 16 | \item{x}{something which can be coerced into a module. \code{character} are 17 | interpreted as file / folder names.} 18 | 19 | \item{...}{arguments passed to \link{parse}} 20 | 21 | \item{topEncl}{(environment) the root of the local search path. It is tried 22 | to find a good default via \link{autoTopEncl}.} 23 | 24 | \item{reInit}{(logical) if a module should be re-initialized} 25 | 26 | \item{envir}{(environment) the environment from where \code{module} is 27 | called. Used to determine the top level environment and should not be 28 | supplied by the use.} 29 | } 30 | \description{ 31 | Interfaces to and from modules. 32 | } 33 | \examples{ 34 | # as.module is used by 'use' so see the vignette for examples: 35 | \dontrun{ 36 | vignette("modulesInR", "modules") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /man/use.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/use.R 3 | \name{use} 4 | \alias{use} 5 | \title{Use a module as dependency} 6 | \usage{ 7 | use(module, ..., attach = FALSE, reInit = TRUE, where = parent.frame()) 8 | } 9 | \arguments{ 10 | \item{module}{(character, module) a file or folder name, or an object that 11 | can be interpreted as a module: any list-like object would do.} 12 | 13 | \item{...}{(character, or unquoted expression) names to use from module.} 14 | 15 | \item{attach}{(logical) whether to attach the module to the search path.} 16 | 17 | \item{reInit}{(logical) we can use a module as is, or reinitialize it. The 18 | default is to reinitialize. This is only relevant should the module be 19 | state-full.} 20 | 21 | \item{where}{(environment) typically the calling environment. Should only be 22 | relevant for testing.} 23 | } 24 | \description{ 25 | Use and/or register a module as dependency. The behaviour of use is similar 26 | to \link{import} but instead of importing from packages, we import from a 27 | module. A module can be defined in a file, or be an object. 28 | } 29 | \details{ 30 | \link{import} and \code{use} can replace \link{library} and \link{attach}. 31 | However they behave differently and are only designed to be used within 32 | modules. Both will work when called in the \code{.GlobalEnv} but here they 33 | should only be used for development and debugging of modules. 34 | 35 | \code{use} adds a layer to a local search path if \code{attach} is 36 | \code{TRUE}. More precisely to the calling environment, which is the 37 | environment supplied by \code{where}. Regardless of the \code{attach} 38 | argument, \code{use} will return the module invisibly. 39 | 40 | \code{use} supplies a special mechanism to find the argument \code{module}: 41 | generally you can supply a file name or folder name as character. You can 42 | also reference objects/names which 'live' outside the module scope. If 43 | names are not found within the scope of the module, they are searched for 44 | in the environment in which the module has been defined. This happens 45 | during initialization of the module, when the \code{use} function is 46 | called. 47 | 48 | Modules can live in files. \code{use} should be used to load them. A module 49 | definition in a file does not need to use the \link{module} constructor 50 | explicitly. Any R script can be used as the body of a module. 51 | 52 | When a folder is referenced in \code{use} it is transformed into a list of 53 | modules. This is represented as a nested list mimicking the folder 54 | structure. Each file in that folder becomes a module. 55 | } 56 | \examples{ 57 | m1 <- module({ 58 | foo <- function() "foo" 59 | }) 60 | m2 <- module({ 61 | use(m1, attach = TRUE) 62 | bar <- function() "bar" 63 | m1foo <- function() foo() 64 | }) 65 | m2$m1foo() 66 | m2$bar() 67 | 68 | \dontrun{ 69 | someFile <- tempfile(fileext = ".R") 70 | writeLines("foo <- function() 'foo'", someFile) 71 | m3 <- use(someFile) 72 | m3$foo() 73 | otherFile <- tempfile(fileext = ".R") 74 | writeLines("bar <- function() 'bar'", otherFile) 75 | m4 <- use(otherFile) 76 | m4$bar() 77 | m5 <- use(tempdir()) 78 | m5 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /man/utilityFunctions.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/getSearchPath.R 3 | \name{getSearchPath} 4 | \alias{getSearchPath} 5 | \alias{getSearchPathNames} 6 | \alias{getSearchPathContent} 7 | \alias{getSearchPathDuplicates} 8 | \title{Get the search path of an environment} 9 | \usage{ 10 | getSearchPath(where = parent.frame()) 11 | 12 | getSearchPathNames(where = parent.frame()) 13 | 14 | getSearchPathContent(where = parent.frame()) 15 | 16 | getSearchPathDuplicates(where = parent.frame()) 17 | } 18 | \arguments{ 19 | \item{where}{(environment | module | function) the object for the search path 20 | should be investigated. If we supply a list with functions (e.g. a module), 21 | the environment of the first function in that list is used.} 22 | } 23 | \description{ 24 | Returns a list with the environments or names of the environments on the 25 | search path. These functions are used for testing, use \link{search} instead. 26 | } 27 | \examples{ 28 | getSearchPath() 29 | getSearchPathNames() 30 | getSearchPathContent() 31 | 32 | m <- module({ 33 | export("foo") 34 | import("stats", "median") 35 | foo <- function() "foo" 36 | bar <- function() "bar" 37 | }) 38 | 39 | getSearchPathContent(m) 40 | 41 | } 42 | -------------------------------------------------------------------------------- /modules.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: No 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: knitr 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace,vignette 22 | -------------------------------------------------------------------------------- /prepareRepo.R: -------------------------------------------------------------------------------- 1 | library(modules) 2 | devtools::install() 3 | devtools::test() 4 | 5 | 6 | devtools::build_vignettes() 7 | knitr::knit("vignettes/modulesInR.Rmd", "README.md") 8 | 9 | text <- c( 10 | "[![Build Status](https://github.com/wahani/modules/actions/workflows/rcmdcheck.yml/badge.svg?branch=main)](https://github.com/wahani/modules/actions)", 11 | "[![codecov.io](https://codecov.io/github/wahani/modules/coverage.svg?branch=master)](https://codecov.io/github/wahani/modules?branch=master)", 12 | "[![CRAN](http://www.r-pkg.org/badges/version/modules)](https://cran.r-project.org/package=modules)", 13 | "![Downloads](http://cranlogs.r-pkg.org/badges/modules)", 14 | "# Modules in R", 15 | readLines( 16 | "README.md" 17 | )[-(1:9)] 18 | ) 19 | 20 | writeLines(text, "README.md") 21 | https://m.tiktok.com/passport/email/unbind/index/?unbind_ticket=vRGFtnMfdskVwHyCwUPmXUEZEXdFZNtx&aid=1233&locale=en&language=en 22 | ## TODO 23 | 24 | ## - depend 25 | ## - on .tar.gz 26 | 27 | 28 | library("modules") 29 | library("parallel") 30 | m <- module({ 31 | import("stats", "median") 32 | }) 33 | 34 | m <- module({ 35 | import("base", "identity", "search") 36 | identity 37 | fun <- function(x) { 38 | identity(x) 39 | } 40 | }) 41 | 42 | mfun <- local(envir = new.env(parent = baseenv()), { 43 | modules::import("base", "identity") 44 | function(x) { 45 | base::identity(x) 46 | } 47 | }) 48 | 49 | modules::getSearchPath(environment(m$fun)) # this setup is slow 50 | modules::getSearchPath(environment(mfun)) # this setup is slow 51 | ## parent.env(parent.env(environment(m$fun))) <- baseenv() # now it is fast 52 | 53 | system.time({ 54 | cl <- makeCluster(2) 55 | clusterMap(cl, m$fun, 1:100) 56 | stopCluster(cl) 57 | }) 58 | 59 | system.time({ 60 | cl <- makeCluster(2) 61 | clusterMap(cl, mfun, 1:100) 62 | stopCluster(cl) 63 | }) 64 | 65 | system.time({ 66 | cl <- makeCluster(2) 67 | clusterMap(cl, identity, 1:100) 68 | stopCluster(cl) 69 | }) 70 | 71 | 72 | # #43 73 | 74 | 75 | 76 | m <- modules::module({ 77 | export("==.foo" = equals) 78 | equals <- function(left, right) {return(left == right)} 79 | }) 80 | 81 | m$"==.foo"(1, 2) 82 | 83 | 84 | m <- modules::module({ 85 | export(true = !FALSE) 86 | }) 87 | m$true 88 | 89 | 90 | library(modules) 91 | modules::module({ 92 | "[" <- function(..., drop = FALSE) .Primitive("[")(..., drop = drop) 93 | }) 94 | -------------------------------------------------------------------------------- /tests/reattachModule.R: -------------------------------------------------------------------------------- 1 | library(modules) 2 | 3 | # We check that a module gets properly re attached, when we load a new version. 4 | # this works inside of a module; but as reported not in .Globalenv: #24 5 | 6 | m <- modules::module({ 7 | fun <- function(x) x 8 | }) 9 | 10 | use(m, attach = TRUE) 11 | 12 | stopifnot(fun(1) == 1) 13 | 14 | m <- modules::module({ 15 | fun <- function(x) x + 1 16 | }) 17 | 18 | use(m, attach = TRUE) 19 | 20 | stopifnot(fun(1) == 2) 21 | -------------------------------------------------------------------------------- /tests/testModule.R: -------------------------------------------------------------------------------- 1 | # a test module for testing download 2 | 3 | fun <- identity 4 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library("modules") 2 | 3 | if (requireNamespace("testthat", quietly = TRUE)) 4 | testthat::test_check("modules") 5 | -------------------------------------------------------------------------------- /tests/testthat/test-amodule.R: -------------------------------------------------------------------------------- 1 | testthat::test_that("Constructors for augmented modules", { 2 | dep <- oldDep <- 1 3 | moduleConst <- function(dep) { 4 | modules::module(topEncl = environment(), { 5 | fun <- function() dep 6 | checkForDep <- function() exists("dep") 7 | changeState <- function() dep <<- 2 8 | }) 9 | } 10 | 11 | m <- moduleConst(dep) 12 | 13 | testthat::expect_equal(m$fun(), dep) 14 | testthat::expect_equal(m$checkForDep(), TRUE) 15 | testthat::expect_equal(m$changeState(), 2) 16 | testthat::expect_equal(dep, oldDep) 17 | 18 | }) 19 | 20 | testthat::test_that("Scoping of parameterized module", { 21 | 22 | dep <- oldDep <- 1 23 | moduleConst <- function(dep) { 24 | amodule({ 25 | fun <- function() dep 26 | checkForDep <- function() exists("dep") 27 | changeState <- function() dep <<- 2 28 | topenv <- function() base::topenv() 29 | }) 30 | } 31 | 32 | m <- moduleConst(dep) 33 | 34 | testthat::expect_equal(m$fun(), dep) 35 | testthat::expect_equal(m$checkForDep(), TRUE) 36 | testthat::expect_equal(m$changeState(), 2) 37 | testthat::expect_equal(dep, oldDep) 38 | testthat::expect_true(identical(m$topenv(), baseenv())) 39 | 40 | }) 41 | -------------------------------------------------------------------------------- /tests/testthat/test-base-override.R: -------------------------------------------------------------------------------- 1 | testthat::test_that("problematic base function calls throw warnings", { 2 | testthat::skip_on_cran() 3 | testthat::expect_warning( 4 | module({ 5 | library(modules) 6 | }) 7 | ) 8 | testthat::expect_warning( 9 | module({ 10 | attach(list()) 11 | }) 12 | ) 13 | testthat::expect_message( 14 | module({ 15 | writeLines("", file <- tempfile()) 16 | source(file) 17 | }) 18 | ) 19 | }) 20 | -------------------------------------------------------------------------------- /tests/testthat/test-depend.R: -------------------------------------------------------------------------------- 1 | testthat::test_that("Packages are installed", { 2 | testthat::skip_on_cran() 3 | testthat::skip_on_ci() 4 | testthat::skip_on_travis() 5 | try(utils::remove.packages("knitr")) 6 | modules::depend("knitr", "1.0.3", repos = "https://cloud.r-project.org") 7 | testthat::expect_true(require("knitr")) 8 | }) 9 | 10 | testthat::test_that("Throw errors", { 11 | 12 | testthat::skip_on_cran() 13 | testthat::skip_on_ci() 14 | testthat::skip_on_travis() 15 | 16 | testthat::expect_is(suppressWarnings( 17 | tmp <- try(modules::depend( 18 | "knitr", "999", 19 | repos = "https://cloud.r-project.org"), 20 | TRUE) 21 | ), "try-error") 22 | testthat::expect_true(grepl("package installation failed", tmp)) 23 | 24 | testthat::expect_is(suppressWarnings( 25 | tmp <- try(modules::depend( 26 | "knitr999", "999", 27 | repos = "https://cloud.r-project.org"), 28 | TRUE) 29 | ), "try-error") 30 | testthat::expect_true(grepl("package installation failed", tmp)) 31 | 32 | }) 33 | -------------------------------------------------------------------------------- /tests/testthat/test-export.R: -------------------------------------------------------------------------------- 1 | testthat::test_that("export can be called savely outside of module #47", { 2 | testthat::expect_warning( 3 | modules::export("something"), 4 | "Calling 'export' outside of a module has no effect." 5 | ) 6 | }) 7 | 8 | test_that("Exports of special names #43", { 9 | m <- module({ 10 | "==.foo" <- function(lhs, rhs) base::`==`(lhs, rhs) # Exclude Linting 11 | "!.foo" <- function(lhs, rhs) base::`!=`(lhs, rhs) # Exclude Linting 12 | }) 13 | testthat::expect_true(m$`==.foo`(1, 1)) 14 | testthat::expect_true(m$`!.foo`(1, 2)) 15 | }) 16 | 17 | test_that("Exports of special names #37", { 18 | m <- module({ 19 | "%+%" <- function(lhs, rhs) lhs + rhs # Exclude Linting 20 | "%add%" <- `%+%` 21 | }) 22 | testthat::expect_true(m$`%+%`(1, 2) == 3) 23 | testthat::expect_true(m$`%add%`(1, 2) == 3) 24 | }) 25 | 26 | test_that("Exports of special names #45", { 27 | m <- module({ 28 | "[" <- `[` 29 | . <- "." 30 | "==" <- "==" 31 | }) 32 | testthat::expect_true(is.primitive(m$"[")) 33 | testthat::expect_true(is.null(m$.)) 34 | testthat::expect_true(m$"==" == "==") 35 | }) 36 | 37 | test_that("Exports of expressions", { 38 | m <- module({ 39 | export( 40 | true = !FALSE, 41 | false = !T 42 | ) 43 | }) 44 | testthat::expect_true(m$true) 45 | testthat::expect_true(!m$false) 46 | }) 47 | 48 | test_that("Exports of names with whitespace #39", { 49 | m <- module({ 50 | "my fun" <- function(x) x # Exclude Linting 51 | "my long fun name" <- function(x) x # Exclude Linting 52 | "1 my fun1" <- function(x) x # Exclude Linting 53 | }) 54 | testthat::expect_true(m$`my fun`(1) == 1) 55 | testthat::expect_true(m$`my long fun name`(1) == 1) 56 | testthat::expect_true(m$`1 my fun1`(1) == 1) 57 | }) 58 | 59 | test_that("Exports of module", { 60 | m <- module({ 61 | fun <- function(x) x 62 | .fun <- function(x) x 63 | pFun <- function(x) x 64 | }) 65 | 66 | testthat::expect_true(all(c("fun", "pFun") %in% names(m))) 67 | testthat::expect_true(!(".fun" %in% names(m))) 68 | 69 | m <- module({ 70 | export(fun) 71 | 72 | fun <- function(x) x 73 | .fun <- function(x) x 74 | pFun <- function(x) x 75 | }) 76 | 77 | testthat::expect_true("fun" %in% names(m)) 78 | testthat::expect_true(!(".fun" %in% names(m))) 79 | testthat::expect_true(!("pFun" %in% names(m))) 80 | 81 | m <- module({ 82 | export(fun, "pFun") 83 | 84 | fun <- function(x) x 85 | .fun <- function(x) x 86 | pFun <- function(x) x 87 | }) 88 | 89 | testthat::expect_true(all(c("fun", "pFun") %in% names(m))) 90 | testthat::expect_true(!(".fun" %in% names(m))) 91 | 92 | m <- module({ 93 | export(fun) 94 | 95 | fun <- function(x) x 96 | .fun <- function(x) x 97 | pFun <- function(x) x 98 | export(pFun) 99 | }) 100 | 101 | testthat::expect_true(all(c("fun", "pFun") %in% names(m))) 102 | testthat::expect_true(!(".fun" %in% names(m))) 103 | }) 104 | 105 | test_that("Produce an error when 'export' is not available", { 106 | testthat::expect_error( 107 | modules::module({ 108 | modules::export("fun", "fun1", "fun2") 109 | fun <- function(x) x 110 | }), 111 | "unable to resolve export: fun1" 112 | ) 113 | }) 114 | 115 | test_that("Rename exports", { 116 | m <- modules::module({ 117 | export( 118 | foo, 119 | a = foo, 120 | b = "foo", 121 | c = function() foo(), 122 | bar = sub$bar 123 | ) 124 | sub <- module({ 125 | bar <- function() "bar" 126 | }) 127 | foo <- function() "foo" 128 | }) 129 | testthat::expect_equal(m$foo(), "foo") 130 | testthat::expect_equal(m$a(), "foo") 131 | testthat::expect_equal(m$b(), "foo") 132 | testthat::expect_equal(m$c(), "foo") 133 | testthat::expect_equal(m$bar(), "bar") 134 | }) 135 | 136 | test_that("Export .names", { 137 | m <- modules::module({ 138 | export(.foo = foo) 139 | foo <- function() "foo" 140 | }) 141 | testthat::expect_equal(m$.foo(), "foo") 142 | }) 143 | 144 | test_that("Warning on duplicate names", { 145 | testthat::expect_warning( 146 | modules::module({ 147 | export(foo, "foo") 148 | foo <- function() "foo" 149 | }), 150 | "duplicate names in exports" 151 | ) 152 | }) 153 | 154 | test_that("Warn with do.call", { 155 | testthat::expect_warning( 156 | modules::module({ 157 | do.call(export, list("foo")) 158 | foo <- function() "foo" 159 | }), 160 | "non standard call to export" 161 | ) 162 | }) 163 | -------------------------------------------------------------------------------- /tests/testthat/test-expose.R: -------------------------------------------------------------------------------- 1 | test_that("exposure of module", { 2 | 3 | expectEqual <- function(a, b) { 4 | testthat::expect_equal(a, b) 5 | } 6 | 7 | m <- module({ 8 | import(modules, module) 9 | m <- module({ 10 | .num <- NULL 11 | set <- function(val) .num <<- val 12 | get <- function() .num 13 | }) 14 | expose(m, get, reInit = FALSE) 15 | }) 16 | 17 | expectEqual(m$m$set(2), m$get()) 18 | expectEqual(names(m), c("get", "m")) 19 | 20 | }) 21 | -------------------------------------------------------------------------------- /tests/testthat/test-extend.R: -------------------------------------------------------------------------------- 1 | testthat::test_that("extend: add method", { 2 | 3 | m <- modules::module({ 4 | fun <- identity 5 | }) 6 | 7 | m <- modules::extend(m, { 8 | fun1 <- identity 9 | }) 10 | 11 | testthat::expect_equal(m$fun1(m$fun(1)), 1) 12 | 13 | }) 14 | 15 | testthat::test_that("extend: override method", { 16 | 17 | m <- modules::module({ 18 | fun <- identity 19 | }) 20 | 21 | m <- modules::extend(m, { 22 | fun <- function(x) 2 * x 23 | }) 24 | 25 | testthat::expect_equal(m$fun(1), 2) 26 | 27 | }) 28 | 29 | testthat::test_that("extend: nested definitions", { 30 | 31 | m <- extend( 32 | extend( 33 | modules::module({ 34 | fun <- identity 35 | }), 36 | {fun <- identity} 37 | ), 38 | {fun <- identity} 39 | ) 40 | 41 | testthat::expect_equal(m$fun(1), 1) 42 | 43 | }) 44 | 45 | testthat::test_that("extend a module from file -- see #16", { 46 | ## just to verify, that standard behavior works: 47 | m <- modules::module({ 48 | export("bar") 49 | bar <- function() "bar" 50 | foo <- function() "foo" 51 | }) 52 | modules::extend(m, { 53 | testthat::expect_equal(foo(), "foo") 54 | }) 55 | 56 | ## now from the file: 57 | fileName <- tempfile(fileext = ".R") 58 | on.exit(file.remove(fileName)) 59 | writeLines(c( 60 | 'export("bar")', 'bar <- function() "bar"', 'foo <- function() "foo"'), 61 | fileName) 62 | m <- modules::use(fileName) 63 | modules::extend(m, { 64 | testthat::expect_equal(foo(), "foo") 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /tests/testthat/test-getSearchPathContent.R: -------------------------------------------------------------------------------- 1 | testthat::test_that("SearchPathContent", { 2 | m <- module({ 3 | export("foo") 4 | import("stats", "median") 5 | foo <- function() "foo" 6 | bar <- function() "bar" 7 | }) 8 | content <- getSearchPathContent(m) 9 | testthat::expect_equal(content[[1]], c("bar", "foo")) 10 | testthat::expect_equal(content[[2]], "median") 11 | }) 12 | 13 | testthat::test_that("Identification of modules", { 14 | # https://github.com/wahani/modules/issues/9#issuecomment-435056847 15 | # we may have functions and data in some use cases 16 | m <- module({ 17 | a <- NULL 18 | b <- function() NULL 19 | }) 20 | content <- getSearchPathContent(m) 21 | testthat::expect_equal(content[[1]], letters[1:2]) 22 | }) 23 | -------------------------------------------------------------------------------- /tests/testthat/test-getSearchPathDuplicates.R: -------------------------------------------------------------------------------- 1 | testthat::test_that("find duplicates on search path", { 2 | 3 | midentity <- module({ 4 | identity <- function(x) x 5 | }) 6 | 7 | testthat::expect_message( 8 | regexp = ".*identity.*", 9 | module({ 10 | import("base", "identity") 11 | }) 12 | ) 13 | 14 | testthat::expect_message( 15 | regexp = ".*identity.*search", 16 | module({ 17 | import("base", "identity", "search") 18 | }) 19 | ) 20 | 21 | testthat::expect_message( 22 | regexp = "(modules:base|modules:midentity).*identity.*(modules:|)base", 23 | m <- module({ 24 | import("base", "identity") 25 | use(midentity, attach = TRUE) 26 | identity <- function(x) x 27 | }) 28 | ) 29 | 30 | testthat::expect_true(all( 31 | getSearchPathDuplicates(m)$identity %in% 32 | c("modules:midentity", "modules:base", "base")) 33 | ) 34 | 35 | testthat::expect_message( 36 | regexp = NA, 37 | module(topEncl = baseenv(), { 38 | import("stats", "median") 39 | }) 40 | ) 41 | 42 | }) 43 | -------------------------------------------------------------------------------- /tests/testthat/test-import.R: -------------------------------------------------------------------------------- 1 | test_that("Import of default packages: #31", { 2 | 3 | # we get errors on some old r release on windows and macos. I can't reproduce 4 | # and do not have access to these OSs. 5 | testthat::skip_on_cran() 6 | 7 | # import default packages, e.g. stats, utils, etc 8 | m <- module(topEncl = baseenv(), { 9 | suppressPackageStartupMessages(importDefaultPackages()) 10 | findsStats <- function() exists("median") 11 | findsUtils <- function() exists("data") 12 | findsGraphics <- function() exists("plot") 13 | findsDatasets <- function() exists("iris") 14 | }) 15 | 16 | for (fun in m) { 17 | testthat::expect_true(fun()) 18 | } 19 | 20 | # now exclude datasets and utils 21 | m <- module(topEncl = baseenv(), { 22 | suppressPackageStartupMessages(importDefaultPackages( 23 | c("datasets", "utils") 24 | )) 25 | findsStats <- function() exists("median") 26 | findsUtils <- function() !exists("data") 27 | findsGraphics <- function() exists("plot") 28 | findsDatasets <- function() !exists("iris") 29 | }) 30 | 31 | for (fun in m) { 32 | testthat::expect_true(fun()) 33 | } 34 | }) 35 | 36 | test_that("Import of datasets: #29", { 37 | # import all datasets from a package 38 | m <- module({ 39 | import("datasets") 40 | getIris <- function() iris 41 | }) 42 | data("iris", envir = environment()) 43 | expect_equal(m$getIris(), iris) 44 | expect_true("iris" %in% getSearchPathContent(m)[["modules:datasets"]]) 45 | # import just one dataset, like any other object 46 | m <- module({ 47 | import("datasets", "iris") 48 | getIris <- function() iris 49 | }) 50 | data("iris", envir = environment()) 51 | expect_equal(m$getIris(), iris) 52 | expect_true("iris" %in% getSearchPathContent(m)[["modules:datasets"]]) 53 | }) 54 | 55 | test_that("Imports of module", { 56 | # import and related functions are part of the parent scope. Not the module 57 | # itself. 58 | m <- module({ 59 | fun <- function() 1 60 | }) 61 | expect_true(Negate(exists)("import", environment(m$fun), inherits = FALSE)) 62 | expect_true(exists("import", environment(m$fun))) 63 | expect_true(Negate(exists)("export", environment(m$fun), inherits = FALSE)) 64 | expect_true(exists("export", environment(m$fun))) 65 | expect_true(Negate(exists)("use", environment(m$fun), inherits = FALSE)) 66 | expect_true(exists("use", environment(m$fun))) 67 | 68 | # imported objects are only available to module 69 | m <- module({ 70 | import(modules, module) 71 | localModule <- module({ 72 | fun <- function(x) x 73 | }) 74 | }) 75 | expect_equal(m$localModule$fun(1), 1) 76 | expect_true(exists("module", environment(m$fun))) 77 | expect_true(Negate(exists)("module", environment(m$fun), inherits = FALSE)) 78 | 79 | m <- module({ 80 | here <- environment() 81 | m <- import("utils", ".S3methods", attach = FALSE) 82 | importPackage <- function() names(m) == ".S3methods" 83 | importPackageAttach <- function() !exists(".S3methods", where = here, inherits = FALSE) 84 | }) 85 | 86 | testthat::expect_true(m$importPackage()) 87 | testthat::expect_true(m$importPackageAttach()) 88 | }) 89 | 90 | test_that("delayed assignment", { 91 | # test for delayed assignment 92 | m <- module({ 93 | import("base", "assignment") # does not exist! 94 | temp <- function() assignment 95 | checkExistens <- function() exists("assignment") 96 | }) 97 | # When 'temp' is called, it should not find 'assignment' 98 | expect_error(m$temp()) 99 | expect_true(m$checkExistens()) 100 | }) 101 | 102 | test_that("package dependencies", { 103 | m <- module({ 104 | import("utils") 105 | deps <- function() exists("packageDescription") 106 | }) 107 | 108 | testthat::expect_true(m$deps()) 109 | testthat::expect_error(module({ 110 | import("DoesNotExist") 111 | }), "'package:DoesNotExist' is not installed!") 112 | }) 113 | 114 | test_that("duplications on search path", { 115 | expectEqual <- function(a, b) { 116 | testthat::expect_equal(a, b) 117 | } 118 | 119 | "%without%" <- function(x, set) { 120 | x[!(x %in% set)] 121 | } 122 | 123 | expectMessage <- function(obj) { 124 | testthat::expect_message(obj) 125 | } 126 | 127 | sp0 <- getSearchPathNames() 128 | 129 | m <- module({ }) 130 | use(m, attach = TRUE) 131 | expectMessage(use(m, attach = TRUE)) 132 | 133 | sp1 <- getSearchPathNames() 134 | 135 | tmp <- tempfile() 136 | writeLines("import(stats) 137 | fun <- function(x) median(x)", tmp) 138 | use(tmp, attach = TRUE) 139 | 140 | sp2 <- getSearchPathNames() 141 | 142 | import(stats) 143 | 144 | sp3 <- getSearchPathNames() 145 | 146 | expectMessage(use(m, attach = TRUE)) 147 | 148 | sp4 <- getSearchPathNames() 149 | 150 | expectEqual(sp1[-1], c("modules:m", sp0[-1])) 151 | expectEqual(sp2[-1], c(paste0("modules:", tmp), sp1[-1])) 152 | expectEqual(sp3[-1], c("modules:stats", sp2[-1])) 153 | expectEqual(sp4[-1], c("modules:m", sp3[-1] %without% "modules:m")) 154 | }) 155 | -------------------------------------------------------------------------------- /tests/testthat/test-lintr.R: -------------------------------------------------------------------------------- 1 | test_that("Package Style", { 2 | ## For some reason these tests fail on mac. 3 | testthat::skip_on_os("mac") 4 | if (requireNamespace("lintr", quietly = TRUE)) { 5 | lintr::expect_lint_free(linters = list( 6 | a = lintr::assignment_linter, 7 | b = lintr::commas_linter, 8 | ## c = lintr::commented_code_linter, 9 | d = lintr::infix_spaces_linter, 10 | e = lintr::line_length_linter(100), 11 | f = lintr::no_tab_linter, 12 | ## g = lintr::snake_case_linter, # detects only testthat functions 13 | h = lintr::object_length_linter(), 14 | i = lintr::spaces_left_parentheses_linter, 15 | j = lintr::trailing_blank_lines_linter, 16 | k = lintr::trailing_whitespace_linter, 17 | l = lintr::open_curly_linter, 18 | m = lintr::object_name_linter(c("CamelCase", "camelCase", "dotted.case")), 19 | n = lintr::closed_curly_linter 20 | )) 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /tests/testthat/test-model-coercion.R: -------------------------------------------------------------------------------- 1 | test_that("as.module", { 2 | 3 | expectEqual <- function(x, y) { 4 | testthat::expect_equal(x, y) 5 | } 6 | 7 | expectTrue <- function(x) { 8 | testthat::expect_true(x) 9 | } 10 | 11 | tmp <- tempfile() 12 | writeLines("fun <- function() 1", tmp) 13 | 14 | expectTrue(exists("fun", as.module(tmp))) 15 | expectTrue(exists( 16 | "fun1", 17 | as.module(tmp, text = "fun1 <- function() 1") 18 | )) 19 | 20 | expectEqual( 21 | environmentName(parent.env(parent.env(environment(as.module(tmp)$fun)))), 22 | "base" 23 | ) 24 | 25 | tmpDir <- paste0(tempdir(), "/test-modules") 26 | dir.create(tmpDir, FALSE) 27 | writeLines("fun1 <- function() 1", paste0(tmpDir, "/tmp1.R")) 28 | writeLines("fun2 <- function() 1", paste0(tmpDir, "/tmp2.r")) 29 | writeLines("fun2 <- function() 1", paste0(tmpDir, "/tmp3")) 30 | 31 | folder <- use(tmpDir) 32 | expectEqual(folder$tmp1$fun1(), 1) 33 | expectEqual(folder$tmp2$fun2(), 1) 34 | expectEqual(length(folder), 2) 35 | 36 | 37 | file.remove(list.files(tmpDir, full.names = TRUE, pattern = "\\.(r|R)$")) 38 | writeLines("fun1 <- function() 1", paste0(tmpDir, "/tmp1.R")) 39 | folder <- use(tmpDir) 40 | 41 | expectEqual(class(folder), "list") 42 | expectEqual(length(folder), 1) 43 | expectEqual(class(folder$tmp1), c("module", "list")) 44 | 45 | m <- module({ 46 | .num <- NULL 47 | set <- function(val) .num <<- val 48 | get <- function() .num 49 | }) 50 | 51 | m$set(2) 52 | expectEqual(m$get(), 2) 53 | 54 | m1 <- as.module(m, reInit = TRUE) 55 | expectTrue(is.null(m1$get())) 56 | 57 | m2 <- as.module(m, reInit = FALSE) 58 | expectEqual(m2$get(), m$get()) 59 | 60 | }) 61 | -------------------------------------------------------------------------------- /tests/testthat/test-module.R: -------------------------------------------------------------------------------- 1 | test_that("Isolation of module", { 2 | # defined objects are local to module 3 | m <- module({ 4 | fun <- function(x) x 5 | }) 6 | expect_true(Negate(exists)("fun")) 7 | expect_true(exists("fun", m)) 8 | 9 | # module does not know of the outside world. This is so in interactive mode. 10 | # In a package it is the enclosing env. The test env is not interactive. 11 | x <- 1 12 | m <- module({ 13 | fun <- function() try(x, silent = TRUE) 14 | }, baseenv()) 15 | expect_is(m$fun(), "try-error") 16 | }) 17 | 18 | test_that("nested modules", { 19 | 20 | expectEqual <- function(a, b) { 21 | testthat::expect_equal(a, b) 22 | } 23 | 24 | val <- module({ 25 | 26 | import(stats, median) 27 | import(modules, module) 28 | 29 | # The nested module should be able to figure out, that it is inside a nested 30 | # module and hence can connect: 31 | m <- module({ 32 | fun <- function(x) median(x) 33 | }) 34 | 35 | })$m$fun(1:10) 36 | 37 | expectEqual( 38 | val, 39 | 5.5 40 | ) 41 | 42 | }) 43 | 44 | test_that("print method for modules", { 45 | 46 | expectOutput <- function(x, expr) { 47 | testthat::expect_output(x, expr) 48 | } 49 | 50 | expectOutput(print(module({ 51 | fun <- function() { 52 | ## doc 53 | NULL 54 | } 55 | })), 56 | "fun:\nfunction\\(\\)\n") 57 | 58 | }) 59 | -------------------------------------------------------------------------------- /tests/testthat/test-print.R: -------------------------------------------------------------------------------- 1 | test_that("modfun in module", { 2 | 3 | expectOutput <- function(x, expr) { 4 | testthat::expect_output(x, expr) 5 | } 6 | 7 | m <- modules::module({ 8 | 9 | oneLineDoc <- function(a, b = "a") { 10 | ## comment 11 | a + b 12 | } 13 | 14 | multiLineDoc <- function(a, b = a) { 15 | ## a numeric 16 | a + b 17 | ## 18 | ## Return: 19 | } 20 | 21 | oneLineFunction <- function(a, b = a) "test" 22 | 23 | obj <- 1:10 24 | 25 | }) 26 | 27 | expectOutput( 28 | print(m), 29 | 'function\\(a, b = \"a\"\\)\n## comment' 30 | ) 31 | 32 | expectOutput( 33 | print(m), 34 | "function\\(a, b = a\\)\n## a numeric\n##\n## Return:" 35 | ) 36 | 37 | expectOutput( 38 | print(m), 39 | "function\\(a, b = a\\)" 40 | ) 41 | 42 | }) 43 | -------------------------------------------------------------------------------- /tests/testthat/test-use.R: -------------------------------------------------------------------------------- 1 | test_that("Re-attach module", { 2 | 3 | outer <- modules::module({ 4 | 5 | m <- modules::module({ 6 | fun <- function(x) x 7 | }) 8 | 9 | use(m, attach = TRUE) 10 | 11 | testthat::expect_equal(fun(1), 1) 12 | 13 | m <- modules::module({ 14 | fun <- function(x) x + 1 15 | }) 16 | 17 | use(m, attach = TRUE) 18 | 19 | testthat::expect_equal(fun(1), 2) 20 | 21 | }) 22 | 23 | }) 24 | 25 | test_that("Attaching other module", { 26 | 27 | expectEqual <- function(a, b) { 28 | testthat::expect_equal(a, b) 29 | } 30 | 31 | m1 <- modules::module({ 32 | 33 | import(modules, module) 34 | 35 | m <- module({ 36 | fun <- function(x) x 37 | }) 38 | 39 | use(m, attach = TRUE) 40 | 41 | funNew <- function(x) fun(x) 42 | 43 | m1 <- module({ 44 | fun <- function(x) x 45 | fun1 <- function(x) x 46 | }) 47 | 48 | use(m1, "fun1", attach = TRUE) 49 | 50 | funNew1 <- function(x) fun1(x) 51 | 52 | }) 53 | 54 | expectEqual(m1$funNew(1), 1) 55 | expectEqual(m1$funNew1(1), 1) 56 | 57 | }) 58 | 59 | test_that("file as module", { 60 | 61 | expectEqual <- function(a, b) { 62 | testthat::expect_equal(a, b) 63 | } 64 | 65 | m <- module({ 66 | tmp <- tempfile() 67 | writeLines("import(stats) 68 | fun <- function(x) median(x)", tmp) 69 | use(tmp, attach = TRUE) 70 | funWithDep <- function(x) fun(x) 71 | }) 72 | 73 | expectEqual(m$funWithDep(1:7), 4) 74 | 75 | }) 76 | 77 | test_that("use finds object in global scope", { 78 | assign("m", list(f = identity), envir = topenv()) 79 | tmp <- function() { 80 | module(topEncl = baseenv(), { 81 | lm <- use(m) 82 | tmp1 <- function() topenv() 83 | tmp2 <- function() exists("m") 84 | tmp3 <- function(x) lm$f(x) 85 | }) 86 | } 87 | 88 | t <- tmp() 89 | testthat::expect_true(identical(t$tmp1(), baseenv())) 90 | testthat::expect_false(t$tmp2()) 91 | testthat::expect_true(identical(t$lm$f, identity)) 92 | rm(list = "m", envir = topenv()) 93 | }) 94 | 95 | test_that("use finds object in global scope", { 96 | m <- modules::module(topEncl = baseenv(), { 97 | error <- try(use(xyz), silent = TRUE) 98 | }) 99 | testthat::expect_is(m$error, "try-error") 100 | testthat::expect_true(grepl("Error in use\\(module = xyz\\)", m$error)) 101 | }) 102 | 103 | test_that("Expose and use are working with package scope", { 104 | m <- modules:::TestModule2() 105 | testthat::expect_identical(m$foo, identity) 106 | testthat::expect_identical(m$b, c) 107 | testthat::expect_identical(m$c, 3) 108 | testthat::expect_identical(m$tm1$b, 3) 109 | testthat::expect_identical(m$tm1$foo, identity) 110 | }) 111 | 112 | test_that("download of module works", { 113 | testthat::skip_on_cran() 114 | m <- use( 115 | "https://raw.githubusercontent.com/wahani/modules/master/tests/testModule.R" 116 | ) 117 | testthat::expect_identical(m$fun, identity) 118 | }) 119 | -------------------------------------------------------------------------------- /vignettes/modulesAsFiles.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Modules: Organizing R Source Code" 3 | date: "`r Sys.Date()`" 4 | output: rmarkdown::html_vignette 5 | vignette: > 6 | %\VignetteIndexEntry{Modules: Organizing R Source Code} 7 | %\VignetteEngine{knitr::rmarkdown} 8 | %\VignetteEncoding{UTF-8} 9 | --- 10 | 11 | ```{r setup, include = FALSE} 12 | knitr::opts_chunk$set( 13 | collapse = TRUE, 14 | comment = "#>" 15 | ) 16 | ``` 17 | 18 | # Introduction 19 | 20 | This vignette explains how to use modules outside of R packages as a means to 21 | organize a project or data analysis. Using modules we may gain some of the 22 | features we also expect from packages but with less overhead. 23 | 24 | A lot of R projects run into problems when they grow. Even relatively simple 25 | data analysis projects can span a thousand lines easily. R has two important 26 | building blocks to organize projects: functions and packages. However packages 27 | do present a hurdle for a lot of users with little programming background. In 28 | those cases we often rely on splitting up the code base into files and *source* 29 | them into our R session (referring to the function `source`). Modules, in this 30 | context, present a more sophisticated way to *source* files by providing three 31 | important features: 32 | 33 | - (Imports) loading a package is local to a module and avoids name clashes in 34 | the global environment. 35 | - (Exports) variable assignment are local to a module and (a) do not pollute the 36 | global environment and (b) hide details of a module. 37 | - Modules make it easy to spread your code base across files and reuse them 38 | when needed. Each file is self contained. 39 | 40 | # Example 41 | 42 | You can load scripts as modules when you refer to a file (or directory) in a 43 | call to `use`. Inside such a script you can use `import` and `use` in the same 44 | way you typically use `library`. Consider the following example where we create 45 | a module in a temporary file with its dependencies. 46 | 47 | ```{r} 48 | code <- " 49 | import('stats', 'median') 50 | functionWithDep <- function(x) median(x) 51 | " 52 | 53 | fileName <- tempfile(fileext = ".R") 54 | writeLines(code, fileName) 55 | ``` 56 | 57 | Then we can load such a module into this session by the following: 58 | 59 | ```{r} 60 | library(modules) 61 | m <- use(fileName) 62 | m$functionWithDep(1:2) 63 | ``` 64 | 65 | # Pseudo-code example 66 | 67 | To give a bit more context of how you can structure a project, consider the 68 | following file structure: 69 | 70 | ``` 71 | / 72 | /R 73 | munging.R 74 | graphics.R 75 | /data 76 | some.csv 77 | /results 78 | /tables 79 | ... 80 | /figs 81 | main.R 82 | README.md 83 | ``` 84 | 85 | You put all your R code into the `R` folder. This folder may or may not have a 86 | nested folder structure itself. You probably have a folder for your data and one 87 | into which you store all results. The important part here is that you have split 88 | your code base into different files. `main.R` in the project root acts as the 89 | *master* file in this example. This file kicks of all steps of our analysis and 90 | *connects the dots*. `munging.R` and `graphics.R` implement helper functions. 91 | 92 | **main.R** 93 | 94 | ```{r eval = FALSE} 95 | lib <- modules::use("R") 96 | dat <- read.csv("data/some.csv") 97 | 98 | # munging 99 | dat <- lib$munging$clean(dat) 100 | dat <- lib$munging$recode(dat) 101 | 102 | # generate results 103 | lib$graphics$barplot(dat) 104 | lib$graphics$lineplot(dat) 105 | ``` 106 | 107 | The `main.R` file implements no logic of the analysis. Its responsibility is to 108 | connect all steps. Each file in the `R` folder then implements a *phase* of the 109 | project. In larger projects it is likely that each phase will need its own 110 | folder. The implementation may then look something along the lines of: 111 | 112 | **R/munging.R** 113 | 114 | ```{r eval = FALSE} 115 | export("clean") 116 | clean <- function(dat) { 117 | # ... 118 | } 119 | 120 | export("recode") 121 | recode <- function(dat) { 122 | # ... 123 | } 124 | 125 | helper <- function(...) { 126 | # This function is private 127 | # ... 128 | } 129 | ``` 130 | 131 | **R/graphics.R** 132 | 133 | ```{r eval = FALSE} 134 | import("ggplot2") 135 | export("barplot", "lineplot") 136 | 137 | barplot <- function(dat) { 138 | # ... 139 | } 140 | 141 | lineplot <- function(dat) { 142 | # ... 143 | } 144 | 145 | helper <- function(...) { 146 | # ... 147 | } 148 | ``` 149 | 150 | - Each file is coerced into a module and can have its own set of imports. They 151 | do not share them. 152 | - Loading the complete folder, or each module individually is a matter of 153 | preference. Loading complete folders saves a couple of lines. 154 | - Each module has its own set of exports. This keeps the interface clean and 155 | minimal. 156 | 157 | # Documentation 158 | 159 | If you want proper documentation for your functions or modules you really want a 160 | package. There are some simple things you can do for ad-hoc 161 | documentation of modules which is to use comments: 162 | 163 | ```{r} 164 | module({ 165 | fun <- function(x) { 166 | ## A function for illustrating documentation 167 | ## x (numeric) some values 168 | x 169 | } 170 | }) 171 | ``` 172 | 173 | # Best practices 174 | 175 | - Modules in files should not load other modules in other files. You should view 176 | a module as a stand alone and self-contained unit. Dependencies should refer to 177 | packages if possible. The benefit is ease of reuse. If your modules do 178 | depend on each other, you use dependency injection to encode these 179 | relationships. See the vignette on *modules as objects*. 180 | - Modules should always declare exports. This clearly communicates which parts 181 | are safe to use and avoids that other parts of our code base rely on 182 | implementation details. 183 | - Do not use `library`, `attach` or `source` inside of modules. It is likely 184 | that they do not do what you want. `import` and `use` are to be preferred in 185 | this context. 186 | - A good length for a module in a file is appr. 100 lines of code. The idea is 187 | to keep things organised and modular. If we only have one big module or a 188 | collection of big modules we do not gain much. 189 | - All other R coding guidelines still apply inside of modules. 190 | - If you need documentation, or want to distribute and publish code: R-Packages 191 | are the way to go. 192 | 193 | -------------------------------------------------------------------------------- /vignettes/modulesAsObjects.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Modules as R Objects" 3 | date: "`r Sys.Date()`" 4 | output: rmarkdown::html_vignette 5 | vignette: > 6 | %\VignetteIndexEntry{Modules as R Objects} 7 | %\VignetteEngine{knitr::rmarkdown} 8 | %\VignetteEncoding{UTF-8} 9 | --- 10 | 11 | ```{r setup, include = FALSE} 12 | knitr::opts_chunk$set( 13 | collapse = TRUE, 14 | comment = "#>" 15 | ) 16 | ``` 17 | 18 | # Introduction 19 | 20 | In this vignette you can find details on 21 | 22 | - how modules can be treated as data and how they are connected to Rs data 23 | types. 24 | - how modules can be viewed as objects as in object orientation. 25 | - and how you can use them inside of packages. 26 | 27 | # Modules as first class citizen in R 28 | 29 | Modules are first class citizens in the sense that they can be treated 30 | like any other data structure in R: 31 | 32 | - they can be created anywhere, including inside another module, 33 | - they can be passed to functions, 34 | - and returned from functions. 35 | 36 | Modules are represented as *list* type in R. Such that 37 | 38 | ```{r} 39 | library("modules") 40 | m <- module({ 41 | foo <- function() "foo" 42 | }) 43 | is.list(m) 44 | class(m) 45 | ``` 46 | 47 | S3 methods may be defined for the class *module*. The package itself only 48 | implements a method for the generic function `print`. 49 | 50 | ## Nested Modules 51 | 52 | Nested modules are modules defined inside other modules. In this case 53 | dependencies of the top level module are accessible to its children: 54 | 55 | ```{r} 56 | m <- module({ 57 | 58 | import("stats", "median") 59 | 60 | anotherModule <- module({ 61 | foo <- function() "foo" 62 | }) 63 | 64 | bar <- function() "bar" 65 | 66 | }) 67 | 68 | getSearchPathContent(m) 69 | getSearchPathContent(m$anotherModule) 70 | ``` 71 | 72 | # Modules as objects 73 | 74 | Sometimes it can be useful to pass arguments to a module. If you have a 75 | background in object oriented programming you may find this natural. From a 76 | functional perspective we define parameters shared by a list of closures. This 77 | is achieved by making the enclosing environment of the module available to the 78 | module itself. 79 | 80 | ```{r} 81 | m <- function(param) { 82 | amodule({ 83 | fun <- function() param 84 | }) 85 | } 86 | m(1)$fun() 87 | ``` 88 | 89 | `amodule` is a wrapper around `module` to abstract the following pattern: 90 | 91 | ```{r} 92 | 93 | m <- function(param) { 94 | module(topEncl = environment(), { 95 | fun <- function() param 96 | }) 97 | } 98 | m(1)$fun() 99 | ``` 100 | 101 | Using one of these approaches you construct a local namespace definition with 102 | the option to pass down some arguments. 103 | 104 | ## Dependency injection 105 | 106 | This can be very useful to handle dependencies between two modules. Instead of: 107 | 108 | ```{r} 109 | a <- module({ 110 | foo <- function() "foo" 111 | }) 112 | 113 | b <- module({ 114 | a <- use(a) 115 | foo <- function() a$foo() 116 | }) 117 | ``` 118 | 119 | which would hard code the dependency, we can write: 120 | 121 | ```{r} 122 | B <- function(a) { 123 | amodule({ 124 | foo <- function() a$foo() 125 | }) 126 | } 127 | b <- B(a) 128 | ``` 129 | 130 | There are many good reasons to follow such a strategy. As an example: consider 131 | the case in which module `a` introduces side effects. By leaving it open as 132 | argument we can later decide what exactly we pass down to the constructor of 133 | `b`. This may be important to us when we want to mock a database, disable 134 | logging or otherwise handle access to external ressources. 135 | 136 | ## Modules to model mutable state 137 | 138 | You can not only put functions into your bag (module) but any R-object. This 139 | includes data: modules can be state-full. To illustrate this we define a 140 | module to encapsulate some value and have a *get* and *set* method for it: 141 | 142 | ```{r} 143 | mutableModule <- module({ 144 | .num <- NULL 145 | get <- function() .num 146 | set <- function(val) .num <<- val 147 | }) 148 | mutableModule$get() 149 | mutableModule$set(2) 150 | ``` 151 | 152 | In the next module we can use `mutableModule` and rebuild the interface to 153 | `.num`. 154 | 155 | ```{r} 156 | complectModule <- module({ 157 | suppressMessages(use(mutableModule, attach = TRUE)) 158 | getNum <- function() get() 159 | set(3) 160 | }) 161 | mutableModule$get() 162 | complectModule$getNum() 163 | ``` 164 | 165 | Depending on your expectations with respect to the above code it comes at a 166 | surprise that we can get and set that value from an attached module; Furthermore 167 | it is not changed in `mutableModule`. This is because `use` will trigger a 168 | re-initialization of any module you plug in. You can override this behaviour: 169 | 170 | ```{r} 171 | complectModule <- module({ 172 | suppressMessages(use(mutableModule, attach = TRUE, reInit = FALSE)) 173 | getNum <- function() get() 174 | set(3) 175 | }) 176 | mutableModule$get() 177 | complectModule$getNum() 178 | ``` 179 | 180 | ## Module composition 181 | 182 | In contrast to systems of object orientation, modules do not provide a formal 183 | mechanism of inheritance. Instead we can use various modes of composition. 184 | Inheritance often is used to reuse code; or to add functionality to an existing 185 | module. 186 | 187 | In this context we may use *parameterized modules*, `use`, `expose` and 188 | `extend`. The first two have already been discussed, as has been dependency 189 | injection as a strategy to encode relationships between modules. 190 | 191 | `expose` is most useful when we want to re-export functions from another module: 192 | 193 | ```{r} 194 | A <- function() { 195 | amodule({ 196 | foo <- function() "foo" 197 | }) 198 | } 199 | 200 | B <- function(a) { 201 | amodule({ 202 | expose(a) 203 | bar <- function() "bar" 204 | }) 205 | } 206 | 207 | B(A())$foo() 208 | B(A())$bar() 209 | ``` 210 | 211 | Here we can easily add functionality to a module, or only reuse parts of it. 212 | Another way to achieve this is to use `extend`. The difference is, that with 213 | `expose` we re-export existing functionality unchanged. With `extend` we add 214 | lines of code to an existing module definition. This means we can (a) override 215 | private members of that module and (b) generally gain access to all 216 | implementation details. Hence the following two definitions are equivalent: 217 | 218 | **Variant A** 219 | 220 | ```{r} 221 | a <- module({ 222 | foo <- function() "foo" 223 | bar <- function() "bar" 224 | }) 225 | 226 | a 227 | ``` 228 | 229 | **Variant B** 230 | 231 | ```{r} 232 | a <- module({ 233 | foo <- function() "foo" 234 | }) 235 | 236 | a <- extend(a, { 237 | bar <- function() "bar" 238 | }) 239 | 240 | a 241 | ``` 242 | 243 | `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`. 244 | 245 | ## Unit tests for modules 246 | 247 | The real use case for `extend` is to add unit tests to a module. You can think 248 | of using one of two patterns: 249 | 250 | **Variant A** 251 | 252 | ```{r} 253 | a <- module({ 254 | foo <- function() "foo" 255 | test <- function() { 256 | stopifnot(foo() == "foo") 257 | } 258 | }) 259 | ``` 260 | 261 | **Variant B** 262 | 263 | ```{r} 264 | a <- module({ 265 | foo <- function() "foo" 266 | }) 267 | extend(a, { 268 | stopifnot(foo() == "foo") 269 | }) 270 | ``` 271 | 272 | The latter alternative will keep the interface clean and gives access to private 273 | member functions. Sometimes this can be very useful for testing. 274 | 275 | 276 | # Modules in Packages 277 | 278 | Of course a good way to write R code is to write packages. Modules inside of 279 | packages make a lot of sense, because also in a package we only have one scope 280 | to work with. Modules provide more options. 281 | 282 | - `modules::module`: will connect to the packages namespace by default. 283 | Functions defined inside modules have access to the internal scope of the 284 | package. 285 | - `modules::amodule`: provides a slightly saver way and requires explicit 286 | registration of objects from the packages namespace. This can happen via 287 | dependency injection or `modules::use`. 288 | 289 | 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. 290 | 291 | As you would avoid using `library` inside of packages, you should also avoid 292 | using `modules::import`. The R package namespace mechanism is more than capable 293 | of handling all dependencies. 294 | -------------------------------------------------------------------------------- /vignettes/modulesInR.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Modules in R" 3 | date: "`r Sys.Date()`" 4 | output: rmarkdown::html_vignette 5 | vignette: > 6 | %\VignetteIndexEntry{Modules in R} 7 | %\VignetteEngine{knitr::rmarkdown} 8 | %\VignetteEncoding{UTF-8} 9 | --- 10 | 11 | ```{r, results='asis', echo=FALSE} 12 | cat(gsub("\\n ", "", packageDescription("modules", fields = "Description", encoding = NA))) 13 | ``` 14 | 15 | ## Installation 16 | 17 | From CRAN: 18 | ```{r, eval=FALSE} 19 | install.packages("modules") 20 | ``` 21 | 22 | From GitHub: 23 | 24 | ```{r, eval=FALSE} 25 | if (require("devtools")) install_github("wahani/modules") 26 | ``` 27 | 28 | # Introduction 29 | 30 | The key idea of this package is to provide a unit of source code which has it's 31 | own scope. The main and most reliable infrastructure for such organizational 32 | units in the R ecosystem is a package. Modules can be used as stand alone, 33 | ad-hoc substitutes for a package or as a sub-unit within a package. 34 | 35 | When modules are defined inside of packages they act as bags of functions (like 36 | objects as in object-oriented-programming). Outside of packages modules define 37 | entities which only know of the base environment, i.e. within a module the base 38 | environment is the only *package* on the *search path*. Also they are always 39 | represented as a list inside R. 40 | 41 | ## Scoping of modules 42 | 43 | We can create a module using the `modules::module` function. A module is similar 44 | to a function definition; it comprises: 45 | 46 | - the body of the module 47 | - the environment in which it is created (defined implicitly) 48 | - the environment used for the search path, in most cases `baseenv()` (defined 49 | implicitly) 50 | 51 | Similar to a function you may supply arguments to a module; see the vignette on 52 | modules as objects on this topic. 53 | 54 | To illustrate the very basic functionality of a module, consider the following 55 | example: 56 | 57 | ```{r} 58 | library("modules") 59 | m <- module({ 60 | foo <- function() "foo" 61 | }) 62 | m$foo() 63 | ``` 64 | 65 | Here `m` is the collection of objects created inside the module. This is a 66 | `list` with the function `foo` as only element. We can do the same thing and define a module in a separate file: 67 | 68 | **module.R** 69 | 70 | ``` 71 | foo <- function() "foo" 72 | ``` 73 | 74 | **main.R** 75 | 76 | ```{r eval = FALSE} 77 | m <- modules::use("module.R") 78 | m$foo() 79 | ``` 80 | 81 | ``` 82 | ## [1] "foo" 83 | ``` 84 | 85 | The two examples illustrate the two ways in which modules can be constructed. 86 | Since modules are isolated from the `.GlobalEnv` the following object `x` can 87 | not be found: 88 | 89 | ```{r error=TRUE} 90 | x <- "hey" 91 | m <- module({ 92 | someFunction <- function() x 93 | }) 94 | m$someFunction() 95 | getSearchPathContent(m) 96 | ``` 97 | 98 | Two features of modules are important at this point: 99 | 100 | - We can keep the global workspace clean, by introducing a local scope 101 | - We have no direct access to the global environment from modules by default, 102 | enforcing discipline when using any form of dependency (objects and packages). 103 | 104 | The following subsections explain how to work with these two features. 105 | 106 | ## Imports 107 | 108 | If you rely on exported objects of a package you can refer to them explicitly 109 | using `::`: 110 | 111 | ```{r} 112 | m <- module({ 113 | functionWithDep <- function(x) stats::median(x) 114 | }) 115 | m$functionWithDep(1:10) 116 | ``` 117 | 118 | Or you can use `import` for *attaching* single objects or packages. Import acts as a substitute for `library` with an important difference: `library` has the side effect of changing the search path of the complete R session. `import` only changes the search path of the calling environment, i.e. the side effect is local to the module and does not affect the global state of the R session. 119 | 120 | ```{r} 121 | m <- module({ 122 | import("stats", "median") # make median from package stats available 123 | 124 | functionWithDep <- function(x) median(x) 125 | }) 126 | m$functionWithDep(1:10) 127 | getSearchPathContent(m) 128 | ``` 129 | 130 | ```{r} 131 | m <- module({ 132 | import("stats") 133 | 134 | functionWithDep <- function(x) median(x) 135 | }) 136 | m$functionWithDep(1:10) 137 | ``` 138 | 139 | ## Importing modules 140 | 141 | To *import* other modules, the function `use` can be called. *use* really just means *import module*. With `use` we can load modules: 142 | 143 | - defined in the calling environment of the module definition 144 | - or defined in files or folders (see the corresponding vignette on this topic) 145 | 146 | Consider the following example: 147 | 148 | ```{r} 149 | mm <- module({ 150 | m <- use(m) 151 | anotherFunction <- function(x) m$functionWithDep(x) 152 | }) 153 | mm$anotherFunction(1:10) 154 | ``` 155 | 156 | To load modules from a file we can refer to the file directly: 157 | 158 | ```{r eval = FALSE} 159 | module({ 160 | m <- use("someFile.R") 161 | # ... 162 | }) 163 | ``` 164 | 165 | ## Exports 166 | 167 | Modules can help to isolate code from the state of the global environment. Now 168 | we may have reduced the complexity in our global environment and moved it into a 169 | module. However, to make it very obvious which parts of a module should be used 170 | we can also define exports. Every non-exported object will not be accessible. 171 | 172 | Properties of exports are: 173 | 174 | - You can list the names of objects in a call to `export`. 175 | - Exports stack up: you can have multiple calls to `export` in a module 176 | definition, i.e. directly in front of each function you want to export. 177 | - Exports can be defined as regular expressions which is indicated by a leading 178 | '^'. In this case only one export declaration should be used. 179 | 180 | ```{r} 181 | m <- module({ 182 | export("fun") 183 | 184 | fun <- identity # public 185 | privateFunction <- identity 186 | 187 | # .named are always private 188 | .privateFunction <- identity 189 | }) 190 | 191 | m 192 | ``` 193 | 194 | # Example: Modules as Parallel Process 195 | 196 | One example where you may want to have more control of the enclosing environment 197 | of a function is when you parallelize your code. First consider the case when a 198 | *naive* implementation fails. 199 | 200 | ```{r error=TRUE} 201 | library("parallel") 202 | dependency <- identity 203 | fun <- function(x) dependency(x) 204 | 205 | cl <- makeCluster(2) 206 | clusterMap(cl, fun, 1:2) 207 | stopCluster(cl) 208 | ``` 209 | 210 | To make the function `fun` self contained we can define it in a module. 211 | 212 | ```{r} 213 | m <- module({ 214 | dependency <- identity 215 | fun <- function(x) dependency(x) 216 | }) 217 | 218 | cl <- makeCluster(2) 219 | clusterMap(cl, m$fun, 1:2) 220 | stopCluster(cl) 221 | ``` 222 | 223 | Note that the parallel computing facilities in `R` always provide a way to 224 | handle such situations. Here it is just a matter of organization if you believe 225 | the function itself should handle its dependencies or the parallel interface. 226 | 227 | 228 | # Related Projects 229 | 230 | There exist several projects with similar goals. First of all, the package 231 | [klmr/modules](https://github.com/klmr/modules) aims at providing a unit similar 232 | to what [Python](https://www.python.org/)-modules are. This project is obviously 233 | interesting for you when you have prior knowledge in Python. `klmr/modules` 234 | modules aim for a full replacement of R-packages. Otherwise there is 235 | considerable overlap of features between the two packages. 236 | 237 | Second you may be interested in 238 | [import](https://cran.r-project.org/package=import) which provides convenient 239 | syntax for stating dependencies in script files. This is something which is also 240 | covered here, although, when you are only interested in a replacement for 241 | `library` the package `import` is more focused. 242 | 243 | `modules` in this package can act as objects as in object-orientation. In 244 | contrast to [R6](https://cran.r-project.org/package=R6) and reference classes 245 | implemented in the methods package here these objects are immutable by default. 246 | Furthermore it is not being made easy to change state of a module; but it is not 247 | difficult to do that if you really want to: see the section on coupling below. 248 | Furthermore inheritance is not a feature, instead you have various possibilities 249 | for object composition. 250 | 251 | The development of the `modules` package has been inspired by other languages: 252 | [F#](https://fsharpforfunandprofit.com/posts/organizing-functions/), 253 | [Erlang](https://learnyousomeerlang.com/modules/) and 254 | [julia](https://docs.julialang.org/en/v1/manual/modules/index.html). 255 | --------------------------------------------------------------------------------