Search code examples
renvironmentr6

How to store a list of variables generated within sourced R code


I am trying to populate the public and private fields of an R6 class using separate sourced files. For that

I have created a small compiler script that sources the files and greps public and private methods (see example below). The compiler then reads in the code and stores the results (which could in principle be stored in multiple files) into a previously defined environment, which is then used to populate the fields of the class.

This works relatively ok. The problem is that, as seen in the example below, the intermediary variables (variable1.1, variable2, etc) are also saved unless cleaned within the sourced script.

#example of sourced code (testSource.R)

public<- list() 

variable1<-"A"

public["variable1"]<-variable1

variable1.1<- "B"

variable2<- paste0(variable1,variable1.1)

public["variable2"]<-variable2

private<-list()

variable3<-"C"

private["variable3"]<-variable3

variable3.1<- "D"

variable4<- paste0(variable3,variable3.1)

private["variable4"]<-variable4
# compiler script

myPath<-"~/random/path/"

myEnv <- new.env()    

file.sources<-list.files(myPath,full.names = TRUE)

sapply(file.sources,\(x){source(x,local=myEnv)})


public_list <- grep("public",names(myEnv),value=TRUE)

private_list <-grep("private",names(myEnv),value=TRUE)

# concatenate public methods

public<- list()

for (pub in public_list){
  public<-c(public,myEnv[[pub]])
}

rm(pub,public_list)

#concatenate private methods

private<- list()

for (pr in private_list){
  private<-c(private,myEnv[[pr]])
}
rm(pr,private_list)

# Create the Class 

myClass<- R6Class("myClass",lock_class = FALSE, lock_objects=FALSE,
                        public= public,
                        private = private)
                          

In the example above I could subset the environment to select only the lists with something like this:

envList<-as.list.environment(myEnv)

envList<-envList[sapply(names(envList), \(x){typeof(envList[[x]])=="list"})]

#...

But this relies on adhering to specific practices (e.g. saving relevant outputs on lists), which may be difficult to enforce with many people working in collaboration.

Is there a better way of saving the variables within the sourced code, without relying on environments as an intermediary step?

(used input from link1 and link2 )

Thanks!


Solution

  • What is quite interesting is that you noticed you needed to limit the scope of the variables into an environment so you can source/delete the variables afterwards.

    In my opinion a much easier way to do this is to use functions since they will encapsulate their own environment and will only export to the outer world what you return.

    But this relies on adhering to specific practices (e.g. saving relevant outputs on lists), which may be difficult to enforce with many people working in collaboration.

    Best to do this is to design a package that everyone in the team relies on and apply the best practices in the package. The user will be happy the code is working and the developer will be happy the practices will be enforced. If you don´t want to build a whole package, just develop functions and ask the team to use the functions instead of writing their own scripts.

    Here is my take on this:

    # --- sources1.R
    
    public<- list() 
    
    variable1<-"A"
    
    public["variable1"]<-variable1
    
    variable1.1<- "B"
    
    variable2<- paste0(variable1,variable1.1)
    
    public["variable2"]<-variable2
    
    private<-list()
    
    variable3<-"C"
    
    private["variable3"]<-variable3
    
    variable3.1<- "D"
    
    variable4<- paste0(variable3,variable3.1)
    
    private["variable4"]<-variable4
    
    # --- sources2.R
    
    public<- list() 
    
    variable1<-"1"
    
    public["variable1"]<-variable1
    
    variable1.1<- "2"
    
    variable2<- paste0(variable1,variable1.1)
    
    public["variable2"]<-variable2
    
    private<-list()
    
    variable3<-"3"
    
    private["variable3"]<-variable3
    private["variable5"] <- "ABCDE"
    
    variable3.1<- "4"
    
    variable4<- paste0(variable3,variable3.1)
    
    private["variable4"]<-variable4
    
    # -- functions
    
    library(R6)
    
    read_source <- function(file) {
      source(file, local = TRUE)  # (local = TRUE) is important or the variables will leak
      list(private = private, public = public)
    }
    
    merge_source <- function(lhs, rhs) {
      
      # -----------------------------------------
      #         merge rhs into lhs
      # -- ideally need to manage collisions but
      # -- this will just overwrite the variables
      # -- for now
      # -----------------------------------------
      
      for(scope in c("private", "public")) {
        for(vars in names(rhs[[scope]])) {
          lhs[[scope]][[vars]] <- rhs[[scope]][[vars]]
        }    
      }
      
      lhs 
    }
    
    read_and_combine <- function(sources) {
      # - simple map + reduce
      scope <- lapply(sources, read_source)
      scope <- Reduce(merge_source, scope)
      scope
    }
    
    
    build_class <- function(class_name, sources) {
      scope <- read_and_combine(sources)
      R6Class(
        class_name,
        lock_class = FALSE,
        lock_objects = FALSE,
        public = scope$public,
        private = scope$private
      )
    }
    
    sources <- c("source1.R", "source2.R")
    my_class <- build_class("myClass", sources)
    my_class
    
    # - you can check no variable from any script leaks into the Global Environment
    
    ls(envir = .GlobalEnv)