Skip to contents
library(tinycodet)
#> Run `?tinycodet::tinycodet` to open the introduction help page of 'tinycodet'.

 

Introduction

One can use a package without attaching (for example using ::), or one can attach a package (for example using library() or require()).

The advantages and disadvantages of using without attaching a package versus attaching a package - at least those relevant for this article - can be compactly presented in the following table:

aspect :: attach
1 prevent masking functions from other packages Yes (+) No (-)
2 prevent masking core R functions Yes (+) No (-)
3 clarify which function came from which package Yes (+) No (-)
4 place/expose functions only in current environment instead of globally Yes (+) No (-)
5 prevent namespace pollution Yes (+) No (-)
6 minimize typing - especially for infix operators
(i.e. typing package::`%op%`(x, y) instead of x %op% y is cumbersome)
No (-) Yes (+)
7 use multiple related packages,
without constantly switching between package prefixes
No (-) Yes (+)
NOTE: + = advantage, - = disadvantage

What tinycodet attempts to do with its import system, is to somewhat find the best of both worlds. It does this by introducing the following functions:

  • import_from(): Import specific objects from a package into the current or specific environment.

  • import_as(): Import a main package, and optionally its re-exports + its minimal dependencies, under a single alias. This essentially combines the attaching advantage of using multiple related packages (row 7 on the table above), whilst keeping most advantages of using without attaching a package.

  • import_ls(): List names of exported objects by category (like “infix operators”, or “replacement operators”, etc.). Can be used in combination with, for example, library() or import_from(). This gives the advantage of less typing (row 6 on the above table).

  • import_data(): Directly return a data set from a package, to allow straight-forward assignment.

The import package system presented here is just another option provided, just like the import and box packages provide their own alternative import systems. Please feel free to completely ignore this article if you’re really adamant on attaching packages using library()/require() :-).

 

import_from

The easiest to understand and most straight-forward import function is the import_from() function. It takes a package, and exposes the specified functions to the current (or a user-specified) environment:

import_from("magrittr", "%>%")
#> Import & method registration complete
lsf.str() # function exists in current environment
#> %>% : function (lhs, rhs)
rm(list = lsf.str()) # remove function from current environment

We can add a prefix to the functions to avoid conflicts:

import_from("dplyr", "select", prefix = "dpr_")
#> Import & method registration complete
ls() # dpr_select is now available in the current environment
#> [1] "dpr_select"

Prefixes will not be added to infix operators or non-functions.

If you want to add the functions to your search path, rather than the current environment, you can do so as follows:

searchenv_add("my_ops") # add new search path environment called "my_ops"
"my_ops" %in% search() # "my_ops" is now part of your search paths
#> [1] TRUE
import_from("dplyr", "select", prefix = "dpr_", env = "my_ops")
#> Import & method registration complete

# list what is present in search path "my_ops":
ls(searchenv_get("my_ops")) 
#> [1] "dpr_select"

# remove search path "my_ops":
searchenv_rm("my_ops")

The differences between import_from() and using library(package, include.only = ...), are as follows:

  • import_from() does not touch your search path unless you want it to.
  • import_from() does not trigger the .onAttach() function in the package, since you’re not attaching the package globally.
  • import_from() allows adding a prefix to the names of regular functions.

 

import_as

The import_as() function imports an R package + its re-exports under an alias, and also imports any specified direct dependencies of the package under the very same alias. It also informs the user which objects from a package will overwrite which objects from other packages, so you will never be surprised.

Here is one example. Lets importtidytable and its main dependency data.table, under the same alias, which I will call “.tdt” (for “tidy data.table”):

import_as(
  .tdt ~ tidytable, deps = "data.table"
) # this creates the .tdt object
#> Import & method registration complete

Functions can now be accessed using the $ operator.
Like .tdt$some_function().

 

import_ls

When aliasing an R package, infix and replacement operators are also imported in the alias. However, it may be cumbersome to use them from the alias:

import_as(.to ~ tinycodet)
.to$`%row~%`(x, mat)
.to$`strfind<-`(x, ..., value)

It may be more convenient to attach or expose these objects separately.

This is where import_ls() comes in.
import_ls() lists all exported objects in a package of a specific type.
These types are supported:

  • “infix”: infix operators
  • “rp”: replacement operators, including their base functions
  • “nonfun”: objects that are not functions
  • “reg”: regular functions.

So we can just attach only the infix operators from, for example, the ‘magrittr’ package like so:

magrittr_infix <- import_ls("magrittr", "infix")
#> c("%!>%", "%$%", "%<>%", "%>%", "%T>%")
library(magrittr, include.only = magrittr_infix)

Notice that the import_ls() function returns a character vector that can be stored in an object for programmatic (in this case the object “magrittr_infix”), and prints a literal piece of code.

This literal piece of code is printed for syntactical clarity.
You see, the code library(magrittr, include.only = magrittr_infix) does not make it obvious - merely from looking at your code - which infix operators exactly are now attached. We can make it explicit by simply copy-pasting the printed code from import_ls() like so:

import_ls("magrittr", "infix")
#> c("%!>%", "%$%", "%<>%", "%>%", "%T>%")
my_copypaste <- c("%!>%", "%$%", "%<>%", "%>%", "%T>%")
library(magrittr, include.only = my_copypaste)

Now, anyone who merely glances at your code immediately sees which operators came from which package, and with very little effort on your part.

For the sake of demonstration, let’s try out each type here:

import_ls("magrittr", "infix")
#> c("%!>%", "%$%", "%<>%", "%>%", "%T>%")
import_ls("stringi", "rp")
#> c("stri_datetime_add", "stri_datetime_add<-", "stri_sub", "stri_sub_all", 
#> "stri_sub_all<-", "stri_sub<-", "stri_subset", "stri_subset_charclass", 
#> "stri_subset_charclass<-", "stri_subset_coll", "stri_subset_coll<-", 
#> "stri_subset_fixed", "stri_subset_fixed<-", "stri_subset_regex", 
#> "stri_subset_regex<-", "stri_subset<-")
import_ls("bit64", "nonfun")
#> Registered S3 method overwritten by 'bit64':
#>   method          from 
#>   print.bitstring tools
#> "NA_integer64_"
import_ls("tibble", "reg")
#> c("add_case", "add_column", "add_row", "as.tibble", "as_data_frame", 
#> "as_tibble", "as_tibble_col", "as_tibble_row", "char", "column_to_rownames", 
#> "data_frame", "data_frame_", "deframe", "enframe", "frame_data", 
#> "frame_matrix", "glimpse", "has_name", "has_rownames", "is.tibble", 
#> "is_tibble", "lst", "lst_", "new_tibble", "num", "obj_sum", "remove_rownames", 
#> "repair_names", "rowid_to_column", "rownames_to_column", "set_char_opts", 
#> "set_num_opts", "set_tidy_names", "size_sum", "tbl_sum", "tibble", 
#> "tibble_", "tibble_row", "tidy_names", "tribble", "trunc_mat", 
#> "type_sum", "validate_tibble", "view")

 

import_data

The import_as() and import_from() functions get all functions from the package namespace. But packages often also have data sets, which are often not part of the namespace.

The data() function in core R can already load data from packages, but this function loads the data into the global environment, instead of returning the data directly, making assigning the data to a specific variable a bit annoying. Therefore, the tinycodet package introduces the import_data() function, which directly returns a data set from a package.

For example, to import the chicago data set from the gamair R package, and assign it directly to a variable (without having to do re-assignment and so on), one simply runs the following:

d <- import_data("gamair", "chicago")
head(d)
#>   death pm10median pm25median  o3median  so2median    time tmpd
#> 1   130 -7.4335443         NA -19.59234  1.9280426 -2556.5 31.5
#> 2   150         NA         NA -19.03861 -0.9855631 -2555.5 33.0
#> 3   101 -0.8265306         NA -20.21734 -1.8914161 -2554.5 33.0
#> 4   135  5.5664557         NA -19.67567  6.1393413 -2553.5 29.0
#> 5   126         NA         NA -19.21734  2.2784649 -2552.5 32.0
#> 6   130  6.5664557         NA -17.63400  9.8585839 -2551.5 40.0

 

Example

One R package that could benefit from the import system introduced by tinycodet, is the dplyr R package. The dplyr R package overwrites core R functions (including base R) and it overwrites functions from pre-installed recommended R packages (such as MASS). I.e.:

rm(list=ls()) # clearing environment again
library(MASS)
library(dplyr) # <- notice dplyr overwrites base R and recommended R packages
#> 
#> Attaching package: 'dplyr'
#> The following object is masked from 'package:MASS':
#> 
#>     select
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union

# detaching dplyr again:
detach("package:dplyr")

Moreover, dplyr‘s function names are sometimes generic enough that there is no obvious way to tell if a function came from dplyr or some other package (for comparison: one can generally recognize stringi functions as they all start with stri_). ’dplyr’ also has quite a large set of dependencies, like ‘tibble’ which ‘dplyr’ was designed to work along with it.

To prevent masking base R functions, and to prevent obscurity regarding which functions come from dplyr & tibble, and which functions come from core R, one could constantly use dplyr:: and tibble::. But constantly switching between package prefixes or aliases is perhaps undesirable.

So here tinycodet’ import_as() function might help. Below is an example where dplyr is imported (including its re-exports), along with tibble (which is a direct dependency), all under one alias which I’ll call “.dpr”. Moreover, all infix operators from magrittr are attached:

import_as(
  .dpr ~ dplyr, deps = "tibble", lib.loc = .libPaths()
)
#> Import & method registration complete

library(magrittr, include.only = import_ls("magrittr", "infix"))

The functions from dplyr can now be used with the .dpr$ prefix. This way, base R functions are no longer overwritten, and it will be clear for someone who reads your code whether functions like the filter() function is the base R filter function, or the dplyr filter function, as the latter would be called as .dpr$filter().

Let’s first run a simple example code with the imported functions:

d <- import_data("dplyr", "starwars")
d %>%
  .dpr$filter(.data$species == "Droid") %>% # notice the ".data" pronoun can be used without problems
  .dpr$select(name, .dpr$ends_with("color"))
#> # A tibble: 6 × 4
#>   name   hair_color skin_color  eye_color
#>   <chr>  <chr>      <chr>       <chr>    
#> 1 C-3PO  NA         gold        yellow   
#> 2 R2-D2  NA         white, blue red      
#> 3 R5-D4  NA         white, red  red      
#> 4 IG-88  none       metal       red      
#> 5 R4-P17 none       silver, red red, blue
#> 6 BB8    none       none        black


male_penguins <- .dpr$tribble(
     ~name,    ~species,     ~island, ~flipper_length_mm, ~body_mass_g,
 "Giordan",    "Gentoo",    "Biscoe",               222L,        5250L,
  "Lynden",    "Adelie", "Torgersen",               190L,        3900L,
  "Reiner",    "Adelie",     "Dream",               185L,        3650L
)

female_penguins <- .dpr$tribble(
     ~name,    ~species,  ~island, ~flipper_length_mm, ~body_mass_g,
  "Alonda",    "Gentoo", "Biscoe",               211,        4500L,
     "Ola",    "Adelie",  "Dream",               190,        3600L,
"Mishayla",    "Gentoo", "Biscoe",               215,        4750L,
)

Notice that the only change made, is that all functions start with .dpr$, the rest is the same. No need for constantly switching between dplyr::..., tibble::... and so on - yet it is still clear from the code that the functions came from the dplyr + tibble family, and there is no fear of overwriting functions from other R packages - let alone core R functions.

 

import_diagnose

The import_ functions always check the specified library paths (argument lib.loc in the import_ functions) before importing a package, and give an error if the package is not present in the specified library paths.

However, there are many ways to load an R package: via ::, via loadNamespace(), or via library()/require() (the latter loads AND attaches a package). For these other functions, if the package is already loaded, no error will be given if the package is not present in the specified library path. A package that is already loaded can not always be safely unloaded.

This may, indirectly, lead to a situation where a package or one of its recursive dependencies have been loaded from a different library path. Worse still, it may be of the wrong version. The ::, loadNamespace() and librar()/require() functions will not inform you about this. If your project relies on strict library path or package version management, this is a problem.

Luckily for the user, the import_ functions DO check if any reverse dependencies has been loaded different library paths or has been loaded from a different version than the version available in the specified library paths, and warns the user if any mismatches are found..

You can run import_diagnose() to see exactly which loaded packages are not aligned with those present in the library path. This allows you to properly examine any problems to your version control endeavour.

Like so:

 

When to use or not to use the new import system

The ‘tinycodet’ import system is helpful particularly for packages that have at least one of the following properties:

  • The namespace of the package(s) conflicts with other packages.

  • The namespace of the package(s) conflicts with core R, or with those of recommended R packages.

  • The package(s) have function names that are generic enough, such that it is not obvious which function came from which package.

There is no necessity for using the ‘tinycodet’ import system with every single package. One can safely attach the ‘stringi’ package, for example, as ‘stringi’ uses a unique and immediately recognisable naming scheme (virtually all ‘stringi’ functions start with “stri_”), and this naming scheme does not conflict with core R, nor with most other packages.