R6 classes, get all fields as named list

846 Views Asked by At

I would like to retrieve the values of all my attributes of R6Class objects as a list. Preferably the values would come with the attribute names. Ideally this could be written generally so that it works for inherited classes as well. Unfortunately the R6 documentation is not so comprehensive and I couldn't find a comparable question.

The following basic example should demonstrate what I currently have and what the target solution should look like.

Person <- R6::R6Class("Person", public = list(
  age = NULL,
  gender = NULL,
  
  initialize = function(age, gender = "M") {
    self$age <- age
    self$gender <- gender
  },
  
  list_attributes = function(){
    return(list(self$age, self$gender))
  }
))

p <- Person$new(age=42, gender="W")
p$list_attributes()

# Output
> [[1]]
> [1] 42

> [[2]]
> [1] "W"

The list_attributes partially does what I want but I think there should be a better way to get all public attributes than explicitly naming all of them. I found that with str(p) I get some information about my object but not in the form I want it.

> str(p)
Classes 'Person', 'R6' <Person>
  Public:
    age: 42
    clone: function (deep = FALSE) 
    gender: W
    initialize: function (age, gender = "M") 
    list_attributes: function () 

My desired outut would like:

list(age=42, gender="M")
$age
[1] 42

$gender
[1] "M"

Does anyone have an idea how to achieve this?

3

There are 3 best solutions below

2
On BEST ANSWER

Thanks for your question and answer, I was struggling with the same issue.

This issue links to a question I asked recently on changing the class of self within an object, here.

As it was explained to me, it is possible to access the object class within the object using class(self), so no need to use a private "class" field, and so your problem can be solved using your object structure as:

Person <- R6::R6Class("Person",                     
                      
                      public = list(
                        age = NULL,
                        gender = NULL,
                        
                        initialize = function(age, gender = "M") {
                          self$age <- age
                          self$gender <- gender
                        },
                        
                        public_fields = function(){
                          return(names(get(class(self))$public_fields))
                        },
                        
                        list_attributes = function(){
                          
                          values <- purrr::map(self$public_fields(), ~.subset2(self, .x))
                          names(values) <- self$public_fields()
                          
                          return(values)
                        }
                      ))

Which gives:

> p <- Person$new(age=42, gender="W")
> p$public_fields()
[1] "age"    "gender"
> p$list_attributes()
$age
[1] 42

$gender
[1] "W"

Or, by putting it all in one active field, and using set_names():

Person <- R6::R6Class("Person",                     
                      
                      public = list(
                        age = NULL,
                        gender = NULL,
                        
                        initialize = function(age, gender = "M") {
                          self$age <- age
                          self$gender <- gender
                        }
                        ),
                      active = list(
                        
                        my_fields = function(){
                          purrr::map(names(get(class(self))$public_fields), ~.subset2(self, .x)) %>% 
                            set_names(names(get(class(self))$public_fields))
                        }
                      ))

Which gives:

> p <- Person$new(age=42, gender="W")
> p$my_fields
$age
[1] 42

$gender
[1] "W"
2
On

The source code of the R6::Class print function helped me a bit to come closer to my desired solution.

Person <- R6::R6Class("Person",
  
  private = list(
    class = Person
  ),                      
  
  public = list(
    age = NULL,
    gender = NULL,
  
    initialize = function(age, gender = "M") {
      self$age <- age
      self$gender <- gender
    },
    
    public_fields = function(){
      return(names(private$class$public_fields))
    },
  
  list_attributes = function(){
    
    values <- purrr::map(self$public_fields(), ~.subset2(self, .x))
    names(values) <- self$public_fields()
    
    return(values)
  }
))

Now the function produces the desired output but I have to specify the class which I have done for now as a private member variable. If this could be done dynamically I would be happy.

The output for now is:

> p <- Person$new(age=42, gender="W")
> p$list_attributes()
$age
[1] 42

$gender
[1] "W"
0
On

In the answers above, extracting the fields is coupled to the class. I preferred to use a function to extract the fields and field values. Hence, I can use it with any R6 class.

Person <- R6::R6Class("Person", public = list(
  age = NULL,
  gender = NULL,
  
  initialize = function(age, gender = "M") {
    self$age <- age
    self$gender <- gender
  }))
p <- Person$new(age=42, gender="W")
R6_extract_values <- function(r6class){
  tmp <- sapply(r6class, class)
  slots <- tmp[ !tmp %in% c("environment", "function")]
  res <- list()
  for (i in names(slots)) {
    if ("R6" %in% class(r6class[[i]])) {
      res[[i]]  <- R6_extract_values(r6class[[i]])
    }else{
      res[[i]] <- r6class[[i]]
    }
  }
  return(res)
}

sapply(r6class, class) does the job (extracts all slots and their class. The rest is to extract the values if the slot is not an environment or function.

and then

> R6_extract_values(p)
$gender
[1] "W"

$age
[1] 42