SAEM PAKEJ, DIFFRINT VERSHUNZ: PAWSIBL?

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.