r-lib / R6

Encapsulated object-oriented programming for R
https://R6.r-lib.org
Other
407 stars 56 forks source link

Assigning values to self within a class without knowing their names #69

Closed sckott closed 9 years ago

sckott commented 9 years ago

I want to make an R6 object that has a function that takes in a file, reads it, then assigns new values to the class that are then available to the user. However, I won't know the names of these values ahead of time. E.g., I want to do something like

Ini <- R6Class("Ini",
   portable = FALSE,
   public = list(
     file = NA,
     parsed = NA,
     initialize = function(file) {
       self$file <- file
     },
     read = function() {
       self$parsed <- read_file(self$file)
       # where self$parsed is a named list
       for (i in seq_along(self$parsed)) {
        self[ names(self$parsed)[i] ] <- self$parsed[[i]]
       } 
       self$parsed # return list to be printed
     },
     print = function() {
       cat(paste0("<file: > ", self$file, ".\n"))
       # other print info
     }
   )
)

I could create a new object like obj <- Ini$new("filepath"), then I want to access those new values like obj$key

But when this gives

Error in self[names(self$parsed)[i]] <- self$parsed[[i]] : 
  object of type 'environment' is not subsettable

I'm sure I'm just missing something easy here...Thanks for the help.

wch commented 9 years ago

I think you need to use [[:

  self[[ names(self$parsed)[i] ]] <- self$parsed[[i]]
sckott commented 9 years ago

That gives

Error in self[[names(self$parsed)[i]]] <- self$parsed[[i]] : 
  cannot add bindings to a locked environment
sckott commented 9 years ago

I set lock_objects = FALSE and that allows me to set new objects on self now

However, those new objects aren't available in the object after creation

gaborcsardi commented 9 years ago

@sckott Why are you adding the parsed stuff as members? Isn't it getting mixed up with the proper methods? E.g. what if you have a file, or parse section in the ini file? I am just missing the intent here.

sckott commented 9 years ago

@gaborcsardi just trying to have the objects at the top level to get at more quickly - but not crucial - a user could still simply get at them indexing to object$parsed$...

gaborcsardi commented 9 years ago

I see. But then you need to name your "ordinary" method specially, e.g. as __parse__ or something, otherwise they get mixed up.

I actually think that this problem fits well to the factory pattern. Your parser class (it does not even have to be an R6 class, a single function would do) could just create objects from another class, the ini class. If you only need a print method, you can define an S3 print outside of the function.

But then if you have no methods at all, then maybe you just want to create a simple list with the factory. I don't really see the point of making ini immutable.

Or, if you want to have the ini data and also methods, in a single object, then you can define a custom $ method. This checks if you want to access the data, and then it uses object$parsed$..., otherwise it just calls the method using base::"$". In this case I would use special method names.

Just thinking....

wch commented 9 years ago

@sckott This works for me. I think it should be similar to what you're doing:

library(R6)
Test <- R6Class("Test",
  portable = FALSE,
  lock_objects = FALSE,
  public = list(
    add = function(name, value) {
      self[[name]] <- value
    }
  )
)

obj <- Test$new()
obj$add("x", 10)
obj$x
#> [1] 10
sckott commented 9 years ago

@wch Nice, thanks. I will need that functionality, so thanks for that.

I think for my original question, I'll just stick with pre-defined object so that the parsed file goes into the parsed object

sckott commented 9 years ago

@gaborcsardi Thanks for the ideas. Right, I want a user to be able to parse a file, then access each value by name, e.g., obj$foo$bar instead of having to do obj$parsed$foo$bar - I think your idea of a function inside that hits obj$parsed should work