From 7ed8fb485c47fefcf211d4f59a75b033c7c6bb48 Mon Sep 17 00:00:00 2001 From: Philipp Baumann Date: Sun, 21 Jan 2024 16:49:29 +0100 Subject: [PATCH 1/3] before breaking changes (R 4.1.3) first --- dev/running_r_or_shell_code_in_nix_from_r.Rmd | 28 ++++++++++-------- .../running-r-or-shell-code-in-nix-from-r.Rmd | 29 +++++++++++-------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/dev/running_r_or_shell_code_in_nix_from_r.Rmd b/dev/running_r_or_shell_code_in_nix_from_r.Rmd index 5ad8070e..10541bfa 100644 --- a/dev/running_r_or_shell_code_in_nix_from_r.Rmd +++ b/dev/running_r_or_shell_code_in_nix_from_r.Rmd @@ -26,15 +26,15 @@ Carefully curated software improves over time, so does R. We pick an example fro - "`as.vector()` gains a `data.frame` method which returns a simple named list, also clearing a long standing 'FIXME' to enable `as.vector(, mode ="list")`. This breaks code relying on `as.vector()` to return the unchanged data frame." -The goal is to illustrate this change in behavior before and after R version 4.2.0. +The goal is to illustrate this change in behavior from R versions 4.1.3 and before to R versions 4.2.0 and later. ### Setting up the software environment with Nix -We first create a isolated directory to prepare for a Nix environment, and write a custom `.Rprofile` file as well. By default, the R derivation in Nixpkgs includes the user library at first position (returned by `.libPaths()`). Startup code written to this local `.Rprofile` will make sure that the system's user library (R_LIBS_USER) is excluded from library paths to load packages from. This is nice to install packages from a Nix-R session environment in ad-hoc and interactive manner. However, this comes at the cost that one needs be aware of potential run-time pollution of packages outside the pool of paths per package from the nix store. On macOS, we experienced a high-chance of segmentation faults when accidentally loading packages and linked system libraries from the system's user library, to give an example. rix::init() writes a configuration that takes care of runtime-pure R package libraries from declaratively defined Nix builds. Additionally, it modifies `.libPaths()` in the running R session. +We first create a isolated directory to prepare for a Nix environment, and write a custom `.Rprofile` file as well. By default, the R derivation in Nixpkgs includes the user library at first position (returned by `.libPaths()`). Startup code written to this local `.Rprofile` will make sure that the system's user library (`R_LIBS_USER`) is excluded from library paths to load packages from. This is nice to install packages from a Nix-R session environment in ad-hoc and interactive manner. However, this comes at the cost that one needs be aware of potential run-time pollution of packages outside the pool of paths per package from the nix store. On macOS, we experienced a high-chance of segmentation faults when accidentally loading packages and linked system libraries from the system's user library, to give an example. `rix::init()` writes a configuration that takes care of runtime-pure R package libraries from declaratively defined Nix builds. Additionally, it modifies `.libPaths()` in the running R session. -```{r, eval=FALSE} +```{r} library("rix") -path_env_1 <- file.path(".", "_env_1_R-4-2-0") +path_env_1 <- file.path(".", "_env_1_R-4-1-3") init( project_path = path_env_1, rprofile_action = "overwrite", @@ -44,9 +44,9 @@ init( Next, we write a `default.nix` file containing Nix expressions that pin R version 4.2.0 from Nixpkgs. -```{r, eval=FALSE} +```{r} rix( - r_ver = "4.2.0", + r_ver = "4.1.3", overwrite = TRUE, project_path = path_env_1 ) @@ -54,18 +54,20 @@ rix( ### Defining and interactively testing custom R code with function(s) -We know have set up the configuration for R 4.2.0 set up in a `default.nix` file in the folder `./_env_1_R-4-2-0`. Since you are sure you are using an R version higher 4.2.0 available on your system, you can check what that `as.vector.data.frame()` S3 method returns a list. +We know have set up the configuration for R 4.1.3 set up in a `default.nix` file in the folder `./_env_1_R-4-1-3`. Since you are sure you are using an R version higher 4.2.0 available on your system, you can check what that `as.vector.data.frame()` S3 method returns a list. -```{r, eval=FALSE} +```{r} df <- data.frame(a = 1:3, b = 4:6) (out <- as.vector(x = df, mode ="list")) ``` +This is is different for R versions 4.1.3 and below, where you should get an identical data frame back. + ### Run functioned up code and investigate results produced in pure Nix R software environments -To formally validate in a 'System-to-Nix' approach that the `out` object is identical since `R` \>= 4.2.0, we define a function that runs the computation above. +To formally validate in a 'System-to-Nix' approach that the `out` object is before `R` \< 4.2.0, we define a function that runs the computation above. -```{r, eval=FALSE} +```{r} df_as_vector <- function(x) { out <- as.vector(x = x, mode = "list") return(out) @@ -97,13 +99,15 @@ out_nix_1 <- with_nix( # compare results of custom codebase with indentical # inputs and different software environments identical(out_system_1, out_nix_1) -# should return `TRUE` if your system's R versions in +# should return `FALSE` if your system's R versions in # current interactive R session is R >= 4.2.0 ``` +### Syntax option for specifying function in `expr` argument of `with_nix()` + As an alternative to wrap your final function with input arguments that produces the results in `function()` or `function(){}`, you can also provide default arguments when assigning the function used as `expr` input like this: -```{r, eval=FALSE} +```{r} df_as_vector <- function(x = df) { out <- as.vector(x = x, mode = "list") return(out) diff --git a/vignettes/running-r-or-shell-code-in-nix-from-r.Rmd b/vignettes/running-r-or-shell-code-in-nix-from-r.Rmd index b57b2d75..a94b948e 100644 --- a/vignettes/running-r-or-shell-code-in-nix-from-r.Rmd +++ b/vignettes/running-r-or-shell-code-in-nix-from-r.Rmd @@ -43,17 +43,17 @@ Carefully curated software improves over time, so does R. We pick an example fro - "`as.vector()` gains a `data.frame` method which returns a simple named list, also clearing a long standing 'FIXME' to enable `as.vector(, mode ="list")`. This breaks code relying on `as.vector()` to return the unchanged data frame." -The goal is to illustrate this change in behavior before and after R version 4.2.0. +The goal is to illustrate this change in behavior from R versions 4.1.3 and before to R versions 4.2.0 and later. ### Setting up the software environment with Nix -We first create a isolated directory to prepare for a Nix environment, and write a custom `.Rprofile` file as well. By default, the R derivation in Nixpkgs includes the user library at first position (returned by `.libPaths()`). Startup code written to this local `.Rprofile` will make sure that the system's user library (R_LIBS_USER) is excluded from library paths to load packages from. This is nice to install packages from a Nix-R session environment in ad-hoc and interactive manner. However, this comes at the cost that one needs be aware of potential run-time pollution of packages outside the pool of paths per package from the nix store. On macOS, we experienced a high-chance of segmentation faults when accidentally loading packages and linked system libraries from the system's user library, to give an example. rix::init() writes a configuration that takes care of runtime-pure R package libraries from declaratively defined Nix builds. Additionally, it modifies `.libPaths()` in the running R session. +We first create a isolated directory to prepare for a Nix environment, and write a custom `.Rprofile` file as well. By default, the R derivation in Nixpkgs includes the user library at first position (returned by `.libPaths()`). Startup code written to this local `.Rprofile` will make sure that the system's user library (`R_LIBS_USER`) is excluded from library paths to load packages from. This is nice to install packages from a Nix-R session environment in ad-hoc and interactive manner. However, this comes at the cost that one needs be aware of potential run-time pollution of packages outside the pool of paths per package from the nix store. On macOS, we experienced a high-chance of segmentation faults when accidentally loading packages and linked system libraries from the system's user library, to give an example. `rix::init()` writes a configuration that takes care of runtime-pure R package libraries from declaratively defined Nix builds. Additionally, it modifies `.libPaths()` in the running R session. -```{r eval = FALSE} +```{r} library("rix") -path_env_1 <- file.path(".", "_env_1_R-4-2-0") +path_env_1 <- file.path(".", "_env_1_R-4-1-3") init( project_path = path_env_1, rprofile_action = "overwrite", @@ -64,9 +64,9 @@ init( Next, we write a `default.nix` file containing Nix expressions that pin R version 4.2.0 from Nixpkgs. -```{r eval = FALSE} +```{r} rix( - r_ver = "4.2.0", + r_ver = "4.1.3", overwrite = TRUE, project_path = path_env_1 ) @@ -74,20 +74,23 @@ rix( ### Defining and interactively testing custom R code with function(s) -We know have set up the configuration for R 4.2.0 set up in a `default.nix` file in the folder `./_env_1_R-4-2-0`. Since you are sure you are using an R version higher 4.2.0 available on your system, you can check what that `as.vector.data.frame()` S3 method returns a list. +We know have set up the configuration for R 4.1.3 set up in a `default.nix` file in the folder `./_env_1_R-4-1-3`. Since you are sure you are using an R version higher 4.2.0 available on your system, you can check what that `as.vector.data.frame()` S3 method returns a list. -```{r eval = FALSE} +```{r} df <- data.frame(a = 1:3, b = 4:6) (out <- as.vector(x = df, mode ="list")) ``` +This is is different for R versions 4.1.3 and below, where you should get an identical data frame back. + + ### Run functioned up code and investigate results produced in pure Nix R software environments -To formally validate in a 'System-to-Nix' approach that the `out` object is identical since `R` \>= 4.2.0, we define a function that runs the computation above. +To formally validate in a 'System-to-Nix' approach that the `out` object is before `R` \< 4.2.0, we define a function that runs the computation above. -```{r eval = FALSE} +```{r} df_as_vector <- function(x) { out <- as.vector(x = x, mode = "list") return(out) @@ -120,14 +123,16 @@ out_nix_1 <- with_nix( # compare results of custom codebase with indentical # inputs and different software environments identical(out_system_1, out_nix_1) -# should return `TRUE` if your system's R versions in +# should return `FALSE` if your system's R versions in # current interactive R session is R >= 4.2.0 ``` +### Syntax option for specifying function in `expr` argument of `with_nix()` + As an alternative to wrap your final function with input arguments that produces the results in `function()` or `function(){}`, you can also provide default arguments when assigning the function used as `expr` input like this: -```{r eval = FALSE} +```{r} df_as_vector <- function(x = df) { out <- as.vector(x = x, mode = "list") return(out) From bd6b0d34351973a997c7ce966ce193cd629cd8b3 Mon Sep 17 00:00:00 2001 From: Philipp Baumann Date: Sun, 21 Jan 2024 17:22:11 +0100 Subject: [PATCH 2/3] start Nix-to-Nix for base R example --- dev/running_r_or_shell_code_in_nix_from_r.Rmd | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/dev/running_r_or_shell_code_in_nix_from_r.Rmd b/dev/running_r_or_shell_code_in_nix_from_r.Rmd index 10541bfa..d8613484 100644 --- a/dev/running_r_or_shell_code_in_nix_from_r.Rmd +++ b/dev/running_r_or_shell_code_in_nix_from_r.Rmd @@ -1,11 +1,11 @@ --- -title: "Running R or shell code in Nix from R" +title: "Running R or Shell Code in Nix from R" output: html_document editor_options: chunk_output_type: console --- -## **Testing Code in Evolving Software Dependency Environments with Confidence** +## **Testing code in evolving software dependency environments with confidence** Adhering to sound versioning practices is crucial for ensuring the reproducibility of software. Despite the expertise in software engineering, the ever-growing complexity and continuous development of new, potentially disruptive features present significant challenges in maintaining code functionality over time. This pertains not only to backward compatibility but also to future-proofing. When code handles critical production loads and relies on numerous external software libraries, it's likely that these dependencies will evolve. Infrastructure-as-code and other DevOps principles shine in addressing these challenges. However, they may appear less approachable and more labor-intensive to set up for the average R developer. @@ -13,7 +13,7 @@ Are you ready to test your custom R functions and system commands in a a differe Let's introduce `with_nix()`. `with_nix()` will evaluate custom R code or shell commands with command line interfaces provided by Nixpkgs in a Nix environment, and thereby bring the read-eval-print-loop feeling. Not only can you evaluate custom R functions or shell commands in Nix environments, but you can also bring the results back to your current R session as R objects. -## **Two Operational Modes of Computations in Environments: 'System-to-Nix' and 'Nix-to-Nix'** +## **Two operational modes of computations in environments: 'System-to-Nix' and 'Nix-to-Nix'** We aim to accommodate various use cases, considering a gradient of declarativity in individual or sets of software environments based on personal preferences. There are two main modes for defining and comparing code running through R and system commands (command line interfaces; CLIs) @@ -83,7 +83,7 @@ Then, we will evaluate this test code through a `nix-shell` R session. This adds 3. **Serialization of Dependent R objects:** Saving them to disk and deserializing them back into the R session's RAM via a temporary folder. This process establishes isolation between two distinct computational environments, accommodating both 'System-to-Nix' and 'Nix-to-Nix' computational modes. Simultaneously, it facilitates the transfer of input arguments, dependencies across the call stack, and outputs of `expr` between the Nix-R and the system's R sessions. -This approach guarantees reproducible side effects and effectively streams messages and errors into the R session. Thereby, the {sys} package facilitates capturing standard outputs and errors as text output messages. +This approach guarantees reproducible side effects and effectively streams messages and errors into the R session. Thereby, the {sys} package facilitates capturing standard outputs and errors as text output messages. Please be aware that `with_nix()` will invoke `nix-shell`, which will itself run `nix-build` in case the Nix derivation (package) for R version 4.1.3 is not yet in your Nix store. This will take a bit of time to get the cache. When you use the `exec_mode == "non-blocking"` argument of `with_nix()`, you will see in your current R console the specific Nix paths that will be downloaded and copied into your Nix store automatically. ```{r, eval=FALSE} # now run it in `nix-shell`; `with_nix()` takes care @@ -117,7 +117,7 @@ df_as_vector <- function(x = df) { Then, you just supply the name of the function to evaluate with default arguments. ```{r, eval=FALSE} -out_nix_1_2 <- with_nix( +out_nix_1_b <- with_nix( expr = function() df_as_vector, # provide name of function program = "R", exec_mode = "non-blocking", # run as background process @@ -129,9 +129,13 @@ out_nix_1_2 <- with_nix( It yields the same results. ```{r, eval=FALSE} -Reduce(f = identical, list(out_system_1, out_nix_1, out_nix_1_2)) +Reduce(f = identical, list(out_system_1, out_nix_1, out_nix_1_b)) ``` +### Comparing `as.vector.data.frame()` for both R versions 4.1.3 and 4.2.0 from Nixpkgs + +Here follows an example a `Nix-to-Nix` solution with two subshells to track the evolution of base R in this specific case. We can verify the breaking changes in case study 1 in more declarative manner when we use both R 4.1.3 and R 4.2.0 from Nixpkgs. Since we already have defined R 4.1.3 in the *`env`*`_1_R-4-1-3` subshell, we can use it as a source environment where with_nix() is launched from. Accordingly, we define the R 4.2.0 environment in a *`env`*`_1_2_R-4-2-0`using Nix via `rix::rix()`. The latter environment will be the target environment where `df_as_vector()` will be evaluated in. + ## **Case study 2: Breaking changes in {stringr} 1.5.0** We add one more layer to the reproducibility of the R ecosystem. User libraries from CRAN or GitHub, one thing that makes R shine is the huge collection of software packages available from the community. From 1748afff580aea65a36fe4d5fffd3085796cef2d Mon Sep 17 00:00:00 2001 From: Philipp Baumann Date: Sun, 21 Jan 2024 23:14:59 +0100 Subject: [PATCH 3/3] add Nix-to-Nix, subfolder per subshell --- dev/running_r_or_shell_code_in_nix_from_r.Rmd | 81 ++++++++++++++-- .../running-r-or-shell-code-in-nix-from-r.Rmd | 97 +++++++++++++++++-- 2 files changed, 161 insertions(+), 17 deletions(-) diff --git a/dev/running_r_or_shell_code_in_nix_from_r.Rmd b/dev/running_r_or_shell_code_in_nix_from_r.Rmd index d8613484..3374c4fc 100644 --- a/dev/running_r_or_shell_code_in_nix_from_r.Rmd +++ b/dev/running_r_or_shell_code_in_nix_from_r.Rmd @@ -28,7 +28,7 @@ Carefully curated software improves over time, so does R. We pick an example fro The goal is to illustrate this change in behavior from R versions 4.1.3 and before to R versions 4.2.0 and later. -### Setting up the software environment with Nix +### Setting up the (R) software environment with Nix We first create a isolated directory to prepare for a Nix environment, and write a custom `.Rprofile` file as well. By default, the R derivation in Nixpkgs includes the user library at first position (returned by `.libPaths()`). Startup code written to this local `.Rprofile` will make sure that the system's user library (`R_LIBS_USER`) is excluded from library paths to load packages from. This is nice to install packages from a Nix-R session environment in ad-hoc and interactive manner. However, this comes at the cost that one needs be aware of potential run-time pollution of packages outside the pool of paths per package from the nix store. On macOS, we experienced a high-chance of segmentation faults when accidentally loading packages and linked system libraries from the system's user library, to give an example. `rix::init()` writes a configuration that takes care of runtime-pure R package libraries from declaratively defined Nix builds. Additionally, it modifies `.libPaths()` in the running R session. @@ -40,6 +40,13 @@ init( rprofile_action = "overwrite", message_type = "simple" ) +list.files(path = path_env_1, all.files = TRUE) +``` + +This will generate the following `.Rprofile` file. + +```{r, echo=FALSE} +cat(readLines(file.path(path_env_1, ".Rprofile")), sep = "\n") ``` Next, we write a `default.nix` file containing Nix expressions that pin R version 4.2.0 from Nixpkgs. @@ -52,20 +59,26 @@ rix( ) ``` +The following expression is written to default.nix in the subfolder `./_env_1_R-4-1-3/`. + +```{r, echo=FALSE} +cat(readLines(file.path(path_env_1, "default.nix")), sep = "\n") +``` + ### Defining and interactively testing custom R code with function(s) We know have set up the configuration for R 4.1.3 set up in a `default.nix` file in the folder `./_env_1_R-4-1-3`. Since you are sure you are using an R version higher 4.2.0 available on your system, you can check what that `as.vector.data.frame()` S3 method returns a list. ```{r} df <- data.frame(a = 1:3, b = 4:6) -(out <- as.vector(x = df, mode ="list")) +as.vector(x = df, mode ="list") ``` This is is different for R versions 4.1.3 and below, where you should get an identical data frame back. ### Run functioned up code and investigate results produced in pure Nix R software environments -To formally validate in a 'System-to-Nix' approach that the `out` object is before `R` \< 4.2.0, we define a function that runs the computation above. +To formally validate in a 'System-to-Nix' approach that the object returned from `as.vector.data.frame()` is before `R` \< 4.2.0, we define a function that runs the computation above. ```{r} df_as_vector <- function(x) { @@ -105,7 +118,7 @@ identical(out_system_1, out_nix_1) ### Syntax option for specifying function in `expr` argument of `with_nix()` -As an alternative to wrap your final function with input arguments that produces the results in `function()` or `function(){}`, you can also provide default arguments when assigning the function used as `expr` input like this: +In the previous code snippet we wrapped the top-level `expr` function with `function()` or `function(){}`. As an alternative, you can also provide default arguments when assigning the function used as `expr` input like this: ```{r} df_as_vector <- function(x = df) { @@ -118,7 +131,7 @@ Then, you just supply the name of the function to evaluate with default argument ```{r, eval=FALSE} out_nix_1_b <- with_nix( - expr = function() df_as_vector, # provide name of function + expr = df_as_vector, # provide name of function program = "R", exec_mode = "non-blocking", # run as background process project_path = path_env_1, @@ -129,12 +142,66 @@ out_nix_1_b <- with_nix( It yields the same results. ```{r, eval=FALSE} -Reduce(f = identical, list(out_system_1, out_nix_1, out_nix_1_b)) +Reduce(f = identical, list(out_nix_1, out_nix_1_b)) ``` ### Comparing `as.vector.data.frame()` for both R versions 4.1.3 and 4.2.0 from Nixpkgs -Here follows an example a `Nix-to-Nix` solution with two subshells to track the evolution of base R in this specific case. We can verify the breaking changes in case study 1 in more declarative manner when we use both R 4.1.3 and R 4.2.0 from Nixpkgs. Since we already have defined R 4.1.3 in the *`env`*`_1_R-4-1-3` subshell, we can use it as a source environment where with_nix() is launched from. Accordingly, we define the R 4.2.0 environment in a *`env`*`_1_2_R-4-2-0`using Nix via `rix::rix()`. The latter environment will be the target environment where `df_as_vector()` will be evaluated in. +Here follows an example a `Nix-to-Nix` solution, with two subshells to track the evolution of base R in this specific case. We can verify the breaking changes in case study 1 in more declarative manner when we use both R 4.1.3 and R 4.2.0 from Nixpkgs. Since we already have defined R 4.1.3 in the *`env`*`_1_R-4-1-3` subshell, we can use it as a source environment where with_nix() is launched from. Accordingly, we define the R 4.2.0 environment in a *`env`*`_1_2_R-4-2-0`using Nix via `rix::rix()`. The latter environment will be the target environment where `df_as_vector()` will be evaluated in. + +```{r} +library("rix") +path_env_1_2 <- file.path(".", "_env_1_2_R-4-2-0") + +init( + project_path = path_env_1_2, + rprofile_action = "overwrite", + message_type = "simple" +) + +rix( + r_ver = "4.2.0", + overwrite = TRUE, + project_path = path_env_1_2, + shell_hook = "R" +) + +list.files(path_env_1_2) +``` + +Now, initiate a new R session as development environment using `nix-shell`. Open a new terminal at the current working directory of your R session. The provided expression `default.nix`. defines R 4.1.3 in a "subfolder per subshell" approach. `nix-shell` will use the expression by `default.nix` and prefer it over any other `.nix` files, except when you put a `shell.nix` file in that folder, which takes precedence. + +```{sh, eval=FALSE} +nix-shell --pure ./_env_1_R-4-1-3 +``` + +After some time downloading caches and doing builds, you will enter an R console session with R 4.1.3. You did not need to type in R first, because we set up a R shell hook via `rix::rix()`. Next, we define again the target function to test in R 4.2.0, too. + +```{r, eval=FALSE} +# current Nix-R session with R 4.1.3 +df_as_vector <- function(x) { + out <- as.vector(x = x, mode = "list") + return(out) +} +(out_nix_1 <- df_as_vector(x = df)) +``` + +```{r, eval=FALSE} +out_nix_1_2 <- with_nix( + expr = function() df_as_vector(x = df), + program = "R", + exec_mode = "non-blocking", # run as background process + project_path = path_env_1_2, + message_type = "simple" # you can do `"verbose"`, too +) +``` + +You can now formally compare the outputs of the computation of the same code in R 4.1.3 vs. R 4.2.0 environments controlled by Nix. + +```{r, eval=FALSE} +identical(out_nix_1, out_nix_1_2) +# yields FALSE +``` ## **Case study 2: Breaking changes in {stringr} 1.5.0** diff --git a/vignettes/running-r-or-shell-code-in-nix-from-r.Rmd b/vignettes/running-r-or-shell-code-in-nix-from-r.Rmd index a94b948e..49459eff 100644 --- a/vignettes/running-r-or-shell-code-in-nix-from-r.Rmd +++ b/vignettes/running-r-or-shell-code-in-nix-from-r.Rmd @@ -20,7 +20,7 @@ library(rix) -## **Testing Code in Evolving Software Dependency Environments with Confidence** +## **Testing code in evolving software dependency environments with confidence** Adhering to sound versioning practices is crucial for ensuring the reproducibility of software. Despite the expertise in software engineering, the ever-growing complexity and continuous development of new, potentially disruptive features present significant challenges in maintaining code functionality over time. This pertains not only to backward compatibility but also to future-proofing. When code handles critical production loads and relies on numerous external software libraries, it's likely that these dependencies will evolve. Infrastructure-as-code and other DevOps principles shine in addressing these challenges. However, they may appear less approachable and more labor-intensive to set up for the average R developer. @@ -29,7 +29,7 @@ Are you ready to test your custom R functions and system commands in a a differe Let's introduce `with_nix()`. `with_nix()` will evaluate custom R code or shell commands with command line interfaces provided by Nixpkgs in a Nix environment, and thereby bring the read-eval-print-loop feeling. Not only can you evaluate custom R functions or shell commands in Nix environments, but you can also bring the results back to your current R session as R objects. -## **Two Operational Modes of Computations in Environments: 'System-to-Nix' and 'Nix-to-Nix'** +## **Two operational modes of computations in environments: 'System-to-Nix' and 'Nix-to-Nix'** We aim to accommodate various use cases, considering a gradient of declarativity in individual or sets of software environments based on personal preferences. There are two main modes for defining and comparing code running through R and system commands (command line interfaces; CLIs) @@ -46,7 +46,7 @@ Carefully curated software improves over time, so does R. We pick an example fro The goal is to illustrate this change in behavior from R versions 4.1.3 and before to R versions 4.2.0 and later. -### Setting up the software environment with Nix +### Setting up the (R) software environment with Nix We first create a isolated directory to prepare for a Nix environment, and write a custom `.Rprofile` file as well. By default, the R derivation in Nixpkgs includes the user library at first position (returned by `.libPaths()`). Startup code written to this local `.Rprofile` will make sure that the system's user library (`R_LIBS_USER`) is excluded from library paths to load packages from. This is nice to install packages from a Nix-R session environment in ad-hoc and interactive manner. However, this comes at the cost that one needs be aware of potential run-time pollution of packages outside the pool of paths per package from the nix store. On macOS, we experienced a high-chance of segmentation faults when accidentally loading packages and linked system libraries from the system's user library, to give an example. `rix::init()` writes a configuration that takes care of runtime-pure R package libraries from declaratively defined Nix builds. Additionally, it modifies `.libPaths()` in the running R session. @@ -59,6 +59,14 @@ init( rprofile_action = "overwrite", message_type = "simple" ) +list.files(path = path_env_1, all.files = TRUE) +``` + +This will generate the following `.Rprofile` file. + + +```{r echo = FALSE} +cat(readLines(file.path(path_env_1, ".Rprofile")), sep = "\n") ``` Next, we write a `default.nix` file containing Nix expressions that pin R version 4.2.0 from Nixpkgs. @@ -72,6 +80,13 @@ rix( ) ``` +The following expression is written to default.nix in the subfolder `./_env_1_R-4-1-3/`. + + +```{r echo = FALSE} +cat(readLines(file.path(path_env_1, "default.nix")), sep = "\n") +``` + ### Defining and interactively testing custom R code with function(s) We know have set up the configuration for R 4.1.3 set up in a `default.nix` file in the folder `./_env_1_R-4-1-3`. Since you are sure you are using an R version higher 4.2.0 available on your system, you can check what that `as.vector.data.frame()` S3 method returns a list. @@ -79,7 +94,7 @@ We know have set up the configuration for R 4.1.3 set up in a `default.nix` file ```{r} df <- data.frame(a = 1:3, b = 4:6) -(out <- as.vector(x = df, mode ="list")) +as.vector(x = df, mode ="list") ``` This is is different for R versions 4.1.3 and below, where you should get an identical data frame back. @@ -87,7 +102,7 @@ This is is different for R versions 4.1.3 and below, where you should get an ide ### Run functioned up code and investigate results produced in pure Nix R software environments -To formally validate in a 'System-to-Nix' approach that the `out` object is before `R` \< 4.2.0, we define a function that runs the computation above. +To formally validate in a 'System-to-Nix' approach that the object returned from `as.vector.data.frame()` is before `R` \< 4.2.0, we define a function that runs the computation above. ```{r} @@ -106,7 +121,7 @@ Then, we will evaluate this test code through a `nix-shell` R session. This adds 3. **Serialization of Dependent R objects:** Saving them to disk and deserializing them back into the R session's RAM via a temporary folder. This process establishes isolation between two distinct computational environments, accommodating both 'System-to-Nix' and 'Nix-to-Nix' computational modes. Simultaneously, it facilitates the transfer of input arguments, dependencies across the call stack, and outputs of `expr` between the Nix-R and the system's R sessions. -This approach guarantees reproducible side effects and effectively streams messages and errors into the R session. Thereby, the {sys} package facilitates capturing standard outputs and errors as text output messages. +This approach guarantees reproducible side effects and effectively streams messages and errors into the R session. Thereby, the {sys} package facilitates capturing standard outputs and errors as text output messages. Please be aware that `with_nix()` will invoke `nix-shell`, which will itself run `nix-build` in case the Nix derivation (package) for R version 4.1.3 is not yet in your Nix store. This will take a bit of time to get the cache. When you use the `exec_mode == "non-blocking"` argument of `with_nix()`, you will see in your current R console the specific Nix paths that will be downloaded and copied into your Nix store automatically. ```{r eval = FALSE} @@ -129,7 +144,7 @@ identical(out_system_1, out_nix_1) ### Syntax option for specifying function in `expr` argument of `with_nix()` -As an alternative to wrap your final function with input arguments that produces the results in `function()` or `function(){}`, you can also provide default arguments when assigning the function used as `expr` input like this: +In the previous code snippet we wrapped the top-level `expr` function with `function()` or `function(){}`. As an alternative, you can also provide default arguments when assigning the function used as `expr` input like this: ```{r} @@ -143,8 +158,8 @@ Then, you just supply the name of the function to evaluate with default argument ```{r eval = FALSE} -out_nix_1_2 <- with_nix( - expr = function() df_as_vector, # provide name of function +out_nix_1_b <- with_nix( + expr = df_as_vector, # provide name of function program = "R", exec_mode = "non-blocking", # run as background process project_path = path_env_1, @@ -156,7 +171,69 @@ It yields the same results. ```{r eval = FALSE} -Reduce(f = identical, list(out_system_1, out_nix_1, out_nix_1_2)) +Reduce(f = identical, list(out_nix_1, out_nix_1_b)) +``` + +### Comparing `as.vector.data.frame()` for both R versions 4.1.3 and 4.2.0 from Nixpkgs + +Here follows an example a `Nix-to-Nix` solution, with two subshells to track the evolution of base R in this specific case. We can verify the breaking changes in case study 1 in more declarative manner when we use both R 4.1.3 and R 4.2.0 from Nixpkgs. Since we already have defined R 4.1.3 in the *`env`*`_1_R-4-1-3` subshell, we can use it as a source environment where with_nix() is launched from. Accordingly, we define the R 4.2.0 environment in a *`env`*`_1_2_R-4-2-0`using Nix via `rix::rix()`. The latter environment will be the target environment where `df_as_vector()` will be evaluated in. + + +```{r} +library("rix") +path_env_1_2 <- file.path(".", "_env_1_2_R-4-2-0") + +init( + project_path = path_env_1_2, + rprofile_action = "overwrite", + message_type = "simple" +) + +rix( + r_ver = "4.2.0", + overwrite = TRUE, + project_path = path_env_1_2, + shell_hook = "R" +) + +list.files(path_env_1_2) +``` + +Now, initiate a new R session as development environment using `nix-shell`. Open a new terminal at the current working directory of your R session. The provided expression `default.nix`. defines R 4.1.3 in a "subfolder per subshell" approach. `nix-shell` will use the expression by `default.nix` and prefer it over any other `.nix` files, except when you put a `shell.nix` file in that folder, which takes precedence. + + +```{sh eval = FALSE} +nix-shell --pure ./_env_1_R-4-1-3 +``` + +After some time downloading caches and doing builds, you will enter an R console session with R 4.1.3. You did not need to type in R first, because we set up a R shell hook via `rix::rix()`. Next, we define again the target function to test in R 4.2.0, too. + + +```{r eval = FALSE} +# current Nix-R session with R 4.1.3 +df_as_vector <- function(x) { + out <- as.vector(x = x, mode = "list") + return(out) +} +(out_nix_1 <- df_as_vector(x = df)) +``` + +```{r eval = FALSE} +out_nix_1_2 <- with_nix( + expr = function() df_as_vector(x = df), + program = "R", + exec_mode = "non-blocking", # run as background process + project_path = path_env_1_2, + message_type = "simple" # you can do `"verbose"`, too +) +``` + +You can now formally compare the outputs of the computation of the same code in R 4.1.3 vs. R 4.2.0 environments controlled by Nix. + + +```{r eval = FALSE} +identical(out_nix_1, out_nix_1_2) +# yields FALSE ``` ## **Case study 2: Breaking changes in {stringr} 1.5.0**