Concerned cat
Concerned cat

It started as a simple exchange on Mastodon. Apparently, what is natural for my nix-infused brain may seem far-fetched for non-nixers. Multiple versions of the same package must be possible. But I actually tried this only once, so I decided to document it. Let’s cut to the chase.

TL;DR

You can indeed install the same package twice (with different versions), as long as they are under different paths. Maybe you’re already familiar with the user library path (e.g. ~/R/4.5/, where you can install packages), and the system one (e.g. /usr/lib/R/site-library, where only root can). There’s no reason why you couldn’t have many more user libraries, and each could have its own {tidyr} package, for example. You just have to add it, like so:

paths_backup <- .libPaths()
.libPaths(c("/home/user/my_fancy_new_R_lib", paths_backup))

You can install another tidyr into that new library.

url_tidyr100 <- "https://cran.r-project.org/src/contrib/Archive/tidyr/tidyr_1.0.0.tar.gz"
install.packages(
    url_tidyr100,
    repos=NULL,
    type="source",
    lib = "/home/user/my_fancy_new_R_lib"
)

Then, you are able to chose which version of a package to load with the lib.loc option of the library() function (check ?library for details). You can also use detach() to remove this package from the search path, and add the other one within the same session.

So how does this look in practice?

It can look very different, depending on your way of working. Since I discovered Nix, I can’t imagine a better way to manage software on my computer, so I’ll use that. To make my life a little easier, I use the {rix} package to demo this. As you see in the third line, I ask it to create an environment with two {tidyr}-s.

rix::rix(
    r_ver = "4.5.0",
    r_pkgs = c("tidyr", "tidyr@1.0.0"),
    ide = "radian",
    overwrite = TRUE,
    project_path = "./",
    print = FALSE
)

This will generate a default.nix file in the current working directory, which is a good starting point, but needs to be modified a bit. I link the final result here. You can download this file, put it in a folder, install nix (don’t worry, it doesn’t break anything), and do a nix-shell command. Now you’re in the exact same environment as me. We could say we’re envbuddies. Awesome, you say? I know.

Once you type radian in the console, you can see that we have a .libPaths() with 49 folders; each package in its own, because that’s how Nix works. Two of those are {tidyr}. Let’s find them!

rpkgs <- installed.packages()

(tidyr_liblocs <- setNames(
    object = rpkgs[rpkgs[,"Package"] == "tidyr","LibPath"],
    nm = paste0("v", rpkgs[rpkgs[,"Package"] == "tidyr","Version"])
))
##                                                              v1.3.1 
## "/nix/store/350xssybshpbpzi1kdryvgwh05kz910l-r-tidyr-1.3.1/library" 
##                                                              v1.0.0 
## "/nix/store/42iyfzgjays8mdk3r9yzplamkjhz2as9-r-tidyr-1.0.0/library"

Between those versions, coercion in the replace_na() function was made illegal, so we can run a quick test.

# Looks familiar?
test_seq <- c(1, 1, 2, 3, NA, 8, 13)

# Let's attach the old version first
library(
    tidyr,
    lib.loc = tidyr_liblocs["v1.0.0"]
)

# Coerce numeric to string
replace_na(test_seq, "5")
## [1] "1"  "1"  "2"  "3"  "5"  "8"  "13"
# Save it for later
old_replace_na <- replace_na

# Let's detach this old tidyr
detach("package:tidyr", character.only = TRUE)
# and attach the newer one
library(
    tidyr,
    lib.loc = tidyr_liblocs["v1.3.1"]
)

# Try to coerce numeric to string
try(replace_na(test_seq, "5"))
## Error in vec_assign(data, missing, replace, x_arg = "data", value_arg = "replace") : 
##   Can't convert `replace` <character> to match type of `data` <double>.
# Save it for later
new_replace_na <- replace_na

detach("package:tidyr", character.only = TRUE)

So this works as expected. But this attach-detach is a bit annoying. Could we use the two functions at the same time, without having to go through a lot of hocus-pocus with the search path? Well, to be honest, I could not really find a good way to avoid having to load pacakge or namespace first. If you do know how to use something like getFromNamespace(), but with lib.loc option, please please please let me know!

For now, I just saved the functions with their enquosures, i.e. their environment in their respective package namespace, which includes other functions that they “see” and use. As such, they can be called in the future without the attach-detach cycle.

try(old_replace_na(test_seq, "5"))
## [1] "1"  "1"  "2"  "3"  "5"  "8"  "13"
try(new_replace_na(test_seq, "5"))
## Error in vec_assign(data, missing, replace, x_arg = "data", value_arg = "replace") : 
##   Can't convert `replace` <character> to match type of `data` <double>.

Caveats

At this point you may be impressed. Don’t worry, this is perfectly normal. However, I have to disappoint you: this won’t work every time, or at least not so “easily”. When I tried the same thing with {tidyr} version 0.7.0, it failed to install because the current {purrr} didn’t export a function it needed. So it would have required further tweaks. I’d have had to install older {purrr}, make it depend on that, etc. I’m not sure if it’s even possible without renaming the whole package.

Originally, I wanted to write this post with {Seurat} v3 vs v4 vs v5. But older versions also failed to install due to broken dependencies, which have been retired from CRAN since. To summarise, this will probably only work for relatively recent and simple packages. Packages with many dependencies or ones that are very old are more likely to be incompatible with their current CRAN depencencies.

In those cases, it’s probably more productive to create a separate environment from a dated snapshot (e.g. rstats-on-nix, Posit Package Manager, {groundhog} or similar). That way you can always test your script, make comparisons or whatever your goal is.

With Nix, this is a very natural thing to do, which ensures that all your dev environments work, and will continue to work. I currently have 8 different versions of R, 5 rstudios and 3 dplyr-s installed. They co-exist peacefully, and never conflict with each other. If something needs to be tested on different R versions, package versions or other libraries, this is a great setup.