diff --git a/lib/linguist/samples.json b/lib/linguist/samples.json index a80a9d62..a278b643 100644 --- a/lib/linguist/samples.json +++ b/lib/linguist/samples.json @@ -684,8 +684,8 @@ ".gemrc" ] }, - "tokens_total": 590058, - "languages_total": 716, + "tokens_total": 590909, + "languages_total": 717, "tokens": { "ABAP": { "*/**": 1, @@ -52295,42 +52295,42 @@ "R": { "df.residual.mira": 1, "<": 24, - "-": 27, - "function": 7, - "(": 60, - "object": 9, + "-": 28, + "function": 14, + "(": 163, + "object": 12, "...": 4, - ")": 60, - "{": 7, + ")": 162, + "{": 35, "fit": 2, "analyses": 1, - "[": 10, - "]": 10, - "return": 7, + "[": 13, + "]": 13, + "return": 8, "df.residual": 2, - "}": 7, + "}": 35, "df.residual.lme": 1, "fixDF": 1, "df.residual.mer": 1, "sum": 1, "object@dims": 1, "*": 2, - "c": 5, + "c": 9, "+": 3, "df.residual.default": 1, "q": 2, "df": 3, - "if": 3, - "is.null": 1, + "if": 13, + "is.null": 2, "mk": 2, - "try": 2, + "try": 3, "coef": 1, - "silent": 2, - "TRUE": 5, + "silent": 3, + "TRUE": 12, "mn": 2, - "f": 5, + "f": 9, "fitted": 1, - "inherits": 2, + "inherits": 6, "|": 3, "NULL": 2, "n": 3, @@ -52338,7 +52338,7 @@ "is.data.frame": 1, "is.matrix": 1, "nrow": 1, - "length": 2, + "length": 3, "k": 3, "max": 1, "SHEBANG#!Rscript": 1, @@ -52347,7 +52347,7 @@ "dates": 3, "matrix": 2, "unlist": 2, - "strsplit": 2, + "strsplit": 3, "ncol": 2, "byrow": 2, "days": 2, @@ -52383,6 +52383,218 @@ "height": 1, "hello": 2, "print": 1, + "#": 42, + "module": 25, + "code": 19, + "available": 1, + "via": 1, + "the": 16, + "environment": 4, + "like": 1, + "it": 3, + "returns.": 1, + "@param": 2, + "an": 1, + "identifier": 1, + "specifying": 1, + "full": 1, + "path": 9, + "search": 5, + "see": 1, + "Details": 1, + "even": 1, + "attach": 11, + "is": 7, + "FALSE": 5, + "optionally": 1, + "attached": 2, + "to": 8, + "of": 1, + "current": 2, + "scope": 1, + "defaults": 1, + ".": 5, + "However": 1, + "in": 6, + "interactive": 2, + "invoked": 1, + "directly": 1, + "from": 3, + "terminal": 1, + "only": 1, + "i.e.": 1, + "not": 4, + "within": 1, + "modules": 4, + "import.attach": 1, + "can": 2, + "be": 7, + "set": 1, + "or": 1, + "depending": 1, + "on": 1, + "user": 1, + "s": 2, + "preference.": 1, + "attach_operators": 3, + "causes": 1, + "emph": 3, + "operators": 3, + "by": 1, + "default": 1, + "path.": 1, + "Not": 1, + "attaching": 1, + "them": 1, + "therefore": 1, + "drastically": 1, + "limits": 1, + "a": 5, + "usefulness.": 1, + "Modules": 1, + "are": 1, + "searched": 1, + "options": 1, + "priority.": 1, + "The": 2, + "directory": 1, + "always": 1, + "considered": 1, + "first.": 1, + "That": 1, + "local": 3, + "file": 1, + "./a.r": 1, + "will": 2, + "loaded.": 1, + "Module": 1, + "names": 2, + "fully": 1, + "qualified": 1, + "refer": 1, + "nested": 1, + "paths.": 1, + "See": 1, + "import": 5, + "executed": 1, + "global": 1, + "effect": 1, + "same.": 1, + "When": 1, + "used": 2, + "globally": 1, + "inside": 1, + "newly": 2, + "outside": 1, + "nor": 1, + "other": 2, + "which": 3, + "might": 1, + "loaded": 4, + "@examples": 1, + "@seealso": 3, + "reload": 3, + "@export": 2, + "substitute": 2, + "stopifnot": 3, + "missing": 1, + "&&": 2, + "module_name": 7, + "getOption": 1, + "else": 4, + "class": 4, + "module_path": 15, + "find_module": 1, + "stop": 1, + "attr": 2, + "message": 1, + "containing_modules": 3, + "module_init_files": 1, + "mapply": 1, + "do_import": 4, + "mod_ns": 5, + "as.character": 3, + "module_parent": 8, + "parent.frame": 2, + "mod_env": 7, + "exhibit_namespace": 3, + "identical": 2, + ".GlobalEnv": 2, + "name": 9, + "environmentName": 2, + "parent.env": 4, + "export_operators": 2, + "invisible": 1, + "is_module_loaded": 1, + "get_loaded_module": 1, + "namespace": 13, + "structure": 3, + "new.env": 1, + "parent": 9, + ".BaseNamespaceEnv": 1, + "paste": 3, + "sep": 3, + "source": 2, + "chdir": 1, + "envir": 5, + "cache_module": 1, + "exported_functions": 2, + "lsf.str": 2, + "list2env": 2, + "sapply": 2, + "get": 2, + "ops": 2, + "is_predefined": 2, + "%": 2, + "is_op": 2, + "prefix": 3, + "||": 1, + "grepl": 1, + "Filter": 1, + "op_env": 4, + "cache.": 1, + "@note": 1, + "Any": 1, + "references": 1, + "remain": 1, + "unchanged": 1, + "and": 2, + "files": 1, + "would": 1, + "have": 1, + "happened": 1, + "without": 1, + "unload": 2, + "should": 2, + "production": 1, + "code.": 1, + "does": 1, + "currently": 1, + "detach": 1, + "environments.": 1, + "Reload": 1, + "given": 1, + "Remove": 1, + "cache": 1, + "forcing": 1, + "reload.": 1, + "reloaded": 1, + "reference": 1, + "unloaded": 1, + "still": 1, + "work.": 1, + "Reloading": 1, + "primarily": 1, + "useful": 1, + "for": 1, + "testing": 1, + "during": 1, + "module_ref": 3, + "rm": 1, + "list": 1, + ".loaded_modules": 1, + "whatnot.": 1, + "assign": 1, "##polyg": 1, "vector": 2, "##numpoints": 1, @@ -62597,7 +62809,7 @@ "Protocol Buffer": 63, "PureScript": 1652, "Python": 5715, - "R": 392, + "R": 1243, "Racket": 331, "Ragel in Ruby Host": 593, "RDoc": 279, @@ -62776,7 +62988,7 @@ "Protocol Buffer": 1, "PureScript": 4, "Python": 7, - "R": 4, + "R": 5, "Racket": 2, "Ragel in Ruby Host": 3, "RDoc": 1, @@ -62826,5 +63038,5 @@ "YAML": 2, "Zephir": 2 }, - "md5": "2fa5b83f62907994200462d10d4b7b70" + "md5": "948328ed3c1f2bbc5be44e8460905c2c" } \ No newline at end of file diff --git a/samples/R/import.r b/samples/R/import.r new file mode 100644 index 00000000..dbccce17 --- /dev/null +++ b/samples/R/import.r @@ -0,0 +1,201 @@ +#' Import a module into the current scope +#' +#' \code{module = import('module')} imports a specified module and makes its +#' code available via the environment-like object it returns. +#' +#' @param module an identifier specifying the full module path +#' @param attach if \code{TRUE}, attach the newly loaded module to the object +#' search path (see \code{Details}) +#' @param attach_operators if \code{TRUE}, attach operators of module to the +#' object search path, even if \code{attach} is \code{FALSE} +#' @return the loaded module environment (invisible) +#' +#' @details Modules are loaded in an isolated environment which is returned, and +#' optionally attached to the object search path of the current scope (if +#' argument \code{attach} is \code{TRUE}). +#' \code{attach} defaults to \code{FALSE}. However, in interactive code it is +#' often helpful to attach packages by default. Therefore, in interactive code +#' invoked directly from the terminal only (i.e. not within modules), +#' \code{attach} defaults to the value of \code{options('import.attach')}, which +#' can be set to \code{TRUE} or \code{FALSE} depending on the user’s preference. +#' +#' \code{attach_operators} causes \emph{operators} to be attached by default, +#' because operators can only be invoked in R if they re found in the search +#' path. Not attaching them therefore drastically limits a module’s usefulness. +#' +#' Modules are searched in the module search path \code{options('import.path')}. +#' This is a vector of paths to consider, from the highest to the lowest +#' priority. The current directory is \emph{always} considered first. That is, +#' if a file \code{a.r} exists both in the current directory and in a module +#' search path, the local file \code{./a.r} will be loaded. +#' +#' Module names can be fully qualified to refer to nested paths. See +#' \code{Examples}. +#' +#' @note Unlike for packages, attaching happens \emph{locally}: if +#' \code{import} is executed in the global environment, the effect is the same. +#' Otherwise, the imported module is inserted as the parent of the current +#' \code{environment()}. When used (globally) \emph{inside} a module, the newly +#' imported module is only available inside the module’s search path, not +#' outside it (nor in other modules which might be loaded). +#' +#' @examples +#' # `a.r` is a file in the local directory containing a function `f`. +#' a = import('a') +#' a$f() +#' +#' # b/c.r is a file in path `b`, containing a function `g`. +#' import('b/c', attach = TRUE) +#' g() # No module name qualification necessary +#' +#' @seealso \code{unload} +#' @seealso \code{reload} +#' @seealso \code{module_name} +#' @export +import = function (module, attach, attach_operators = TRUE) { + module = substitute(module) + stopifnot(inherits(module, 'name')) + + if (missing(attach)) { + attach = if (interactive() && is.null(module_name())) + getOption('import.attach', FALSE) + else + FALSE + } + + stopifnot(class(attach) == 'logical' && length(attach) == 1) + + module_path = try(find_module(module), silent = TRUE) + + if (inherits(module_path, 'try-error')) + stop(attr(module_path, 'condition')$message) + + containing_modules = module_init_files(module, module_path) + mapply(do_import, names(containing_modules), containing_modules) + + mod_ns = do_import(as.character(module), module_path) + module_parent = parent.frame() + mod_env = exhibit_namespace(mod_ns, as.character(module), module_parent) + + if (attach) { + if (identical(module_parent, .GlobalEnv)) + attach(mod_env, name = environmentName(mod_env)) + else + parent.env(module_parent) = mod_env + } + else if (attach_operators) + export_operators(mod_ns, module_parent) + + invisible(mod_env) +} + +do_import = function (module_name, module_path) { + if (is_module_loaded(module_path)) + return(get_loaded_module(module_path)) + + # The namespace contains a module’s content. This schema is very much like + # R package organisation. + # A good resource for this is: + # + namespace = structure(new.env(parent = .BaseNamespaceEnv), + name = paste('namespace', module_name, sep = ':'), + path = module_path, + class = c('namespace', 'environment')) + local(source(attr(environment(), 'path'), chdir = TRUE, local = TRUE), + envir = namespace) + cache_module(namespace) + namespace +} + +exhibit_namespace = function (namespace, name, parent) { + exported_functions = lsf.str(namespace) + # Skip one parent environment because this module is hooked into the chain + # between the calling environment and its ancestor, thus sitting in its + # local object search path. + structure(list2env(sapply(exported_functions, get, envir = namespace), + parent = parent.env(parent)), + name = paste('module', name, sep = ':'), + path = module_path(namespace), + class = c('module', 'environment')) +} + +export_operators = function (namespace, parent) { + # `$` cannot be overwritten, but it is generic so S3 variants of it can be + # defined. We therefore test it as well. + ops = c('+', '-', '*', '/', '^', '**', '&', '|', ':', '::', ':::', '$', '=', + '<-', '<<-', '==', '<', '<=', '>', '>=', '!=', '~', '&&', '||') + + is_predefined = function (f) f %in% ops + + is_op = function (f) { + prefix = strsplit(f, '\\.')[[1]][1] + is_predefined(prefix) || grepl('^%.*%$', prefix) + } + + operators = Filter(is_op, lsf.str(namespace)) + name = module_name(namespace) + # Skip one parent environment because this module is hooked into the chain + # between the calling environment and its ancestor, thus sitting in its + # local object search path. + op_env = structure(list2env(sapply(operators, get, envir = namespace), + parent = parent.env(parent)), + name = paste('operators', name, sep = ':'), + path = module_path(namespace), + class = c('module', 'environment')) + + if (identical(parent, .GlobalEnv)) + attach(op_env, name = environmentName(op_env)) + else + parent.env(parent) = op_env +} + +#' Unload a given module +#' +#' Unset the module variable that is being passed as a parameter, and remove the +#' loaded module from cache. +#' @param module reference to the module which should be unloaded +#' @note Any other references to the loaded modules remain unchanged, and will +#' still work. However, subsequently importing the module again will reload its +#' source files, which would not have happened without \code{unload}. +#' Unloading modules is primarily useful for testing during development, and +#' should not be used in production code. +#' +#' \code{unload} does not currently detach environments. +#' @seealso \code{import} +#' @seealso \code{reload} +#' @export +unload = function (module) { + stopifnot(inherits(module, 'module')) + module_ref = as.character(substitute(module)) + rm(list = module_path(module), envir = .loaded_modules) + # unset the module reference in its scope, i.e. the caller’s environment or + # some parent thereof. + rm(list = module_ref, envir = parent.frame(), inherits = TRUE) +} + +#' Reload a given module +#' +#' Remove the loaded module from the cache, forcing a reload. The newly reloaded +#' module is assigned to the module reference in the calling scope. +#' @param module reference to the module which should be unloaded +#' @note Any other references to the loaded modules remain unchanged, and will +#' still work. Reloading modules is primarily useful for testing during +#' development, and should not be used in production code. +#' +#' \code{reload} does not work correctly with attached environments. +#' @seealso \code{import} +#' @seealso \code{unload} +#' @export +reload = function (module) { + stopifnot(inherits(module, 'module')) + module_ref = as.character(substitute(module)) + module_path = module_path(module) + module_name = module_name(module) + rm(list = module_path, envir = .loaded_modules) + #' @TODO Once we have `attach`, need also to take care of the search path + #' and whatnot. + mod_ns = do_import(module_name, module_path) + module_parent = parent.frame() + mod_env = exhibit_namespace(mod_ns, module_ref, module_parent) + assign(module_ref, mod_env, envir = module_parent, inherits = TRUE) +}