r-lib / R6

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

[question] How to enforce type of the fields in an R6 class #48

Closed wligtenberg closed 7 years ago

wligtenberg commented 9 years ago

I would like to use R6 classes for my project. One of the things I like about object oriented programming, is that you can enforce objects to have certain fields of specific types. So that you can make sure that objects that are created are valid. I tried the following, but that doesn't seem to work: library(R6)

Person <- R6Class("Person",
    public = list(
        name = character(),
        age = numeric(),
        initialize = function(name, age) {
          if (!missing(name)) self$name <- name
          if (!missing(age)) self$age <- age
          self$greet()
        },
        set_age = function(val) {
          self$age <- val
        },
        greet = function() {
          cat(paste0("Hello, my name is ", self$name, ".\n"))
        }
    )
)

ann <- Person$new("Ann", "fourteen")
ann

As you can see, I can still provide a character string as an argument to create the new instance. If it is possible to restrict field to a certain class/type, could you please provide an example?

gaborcsardi commented 9 years ago

I think you need to check the arguments of the constructor if you want this, e.g.

...
        initialize = function(name, age) {
          if (!missing(name)) {
            stopifnot(is.character(name), length(name) == 1)
            self$name <- name
          }
          if (!missing(age)) {
            stopifnot(is.numeric(age), length(age) == 1)
            self$age <- age
          }
          self$greet()
        },
...
wch commented 9 years ago

Yes, @gaborcsardi's solution should work for you. Alternately, you could use an active binding, if you want to enforce that the field always has the correct type, but there is a performance penalty for this. (I believe if you use Reference Classes, this is how it operates internally.)

In some of the examples, I do have code like this:

Person <- R6Class("Person",
    public = list(
        name = character(),
        ...

but the character() really is just a placeholder to make the code more readable; it doesn't actually enforce type. Perhaps I should make that more clear in the documentation.

wligtenberg commented 9 years ago

OK, so there is no way to have the classes enforce the type of the fields. I would always have to do it myself. What would be worse performance wise? Using an active binding, or making all field private and force people to use get/set methods, where the set methods (and the constructor) would check the types? If it doesn't matter, then the active bindings are at least more user friendly.

But then again, part of the reason to move from S4 to R6 is that I would like the object creation to be faster, so I don't want to incur too many performance penalties.

wligtenberg commented 9 years ago

Follow up question, would you be willing to add this type enforcement to fields in the future? Or is it too much effort, or you want them to stay light-weight?

thomasp85 commented 9 years ago

Have in mind that this even isn't really possible with the S4 system - the way it is currently done is that the class developer makes a validity function that checks if the object conforms. The developer is then responsible for calling .validObject within all setter functions (anything can be assigned directly with the @ accessor and no checks are made). This could easily be replicated in R6 using active bindings as stated above, and does not require any additions to the current implementation of R6.

thomasp85 commented 9 years ago

Come to think of it, it could be possible to have this done automagically. In the private definition you could have something like:

# (truncated)
private = list(
  number=enforce('numeric', 10),
  string=enforce('character', set=FALSE),
  normalField=1:10
)
# (truncated)

The generator function could then look for fields that had been defined with the enforce function and create the relevant active bindings automatically. The enforce function would take the name of the accepted class, an optionally initial vale and a boolean indicating whether it should be possible to set the value.

@wch If this is something that sound good to you, I can make a PR for it. I'm aware that you're trying to keep R6 minimal and efficient so it is up to you...

wligtenberg commented 9 years ago

@thomasp85 something like that would be quite nice. If I am not mistaken, it using the functionality that is already there, but exposes it using a nice concise interface.

thomasp85 commented 9 years ago

@wligtenberg - that's the idea. It could put a little overhead on object construction because some additional checks are needed, but I believe these will be minor compared to a hard coded class definition - possibly all of this can be moved to the constructor construction, so that the end result would be a constructor with equal performance as a hard coded equivalent (one defining the active bindings directly)...

hadley commented 7 years ago

This is a good idea, but out of scope for R6, which is deliberately minimal. But I don't think there's anything holding you back from implementing in another package as a wrapper around R6.

nhamilton1980 commented 6 years ago

I have done something to the same effect, but a little different, and it works very well.

Build an R6 'Checker' class like the following, here below I only include one such check, but in my class, there is a whole stack of theme probably 20 or 30 different types of checks that I use frequently, check of type, or range, or valid values, etc....

R6Checker = R6::R6Class('Checker',inherit=NULL,
  public = list(
    #Throws error if x is not numeric
    checkNumeric = function(x,objnm = deparse(substitute(x))){
      `if`(!all(is.numeric(x)), stop(sprintf("'%s' must be numeric",objnm),call.=FALSE), invisible(x))
    }
  )
)

Basically, the checker class contains a library of regular checks that you use repetitively and frequently within implementing classes.

Then in the actual class, where you want to enforce something, you can do it like this (it is also possible to inherit from the checker):

R6MyClass = R6::R6Class('MyClass',inherit=NULL,
  public = list(
    initialize = function(years){
      self$setYears(years)
    },
    setYears = function(years){
      private$varYears = private$check$checkNumeric(years) ##<<<<<<< PERFORM THE CHECK
      invisible(self)
    }
  ),
  private = list(
    check = R6Checker$new(), ##<<<<<<< INSTANCE OF CHECKER
    varYears = NULL
  ),
  active = list(
    years = function(years){
      if(missing(years)) return(private$varYears)
      self$setYears(years)
    }
  )
)

Build a wrapper function to make documentation easy, and construction convenient:

MyClass = function(years){
  invisible(R6MyClass$new(years))
}

Now Test:

#The Following is OK
x = MyClass(years = 1:10); 
x$years
#> [1]  1  2  3  4  5  6  7  8  9 10

#The Following will throw an error
x$years = "ABC"
#> Error: 'years' must be numeric 

#The Following will throw an error
x = MyClass(years = "DEF") 
#> Error: 'years' must be numeric 

I'm thinking of making a rather trivial package with the checker class only, as I keep using it over and over and over again for multiple projects.

chrisknoll commented 2 years ago

@thomasp85 , do you think you could fork R6 and put in the implantation you suggest? Contrary to @hadley comments, this would fit in perfectly into a class system. Establishing a syntax like enforce that is baked into the R6 ecosystem would eliminate the need to have several different implementations and approaches to accomplish the same goal (as several commenters have provided their own ideas and implementations).

What's nice about yours is that it puts the enforcement logic very close to where the field is defined vs somewhere else where the field is assigned. When you get beyond some of the trivial examples of more cplicated objects, you really appreciate that you can see the constraint on the field type directly where the field is being defined. In fact, it's making enforcing a type an integral part of defining the field itself, which is really attractive.

thomasp85 commented 2 years ago

I'm afraid I don't have the development bandwidth for this right now