Search code examples
roopr6

Modify behaviour of a R6 class' initialize method depending on input type


My goal is to modify the behavior of the initialize method depending on the type of the argument. For example, if I call MyClass$new(list()) I'd like to initialize my object with a list, and if I call MyClass$new("foo") I'd like to initialize the object with a character string. I think S3 generics are very useful for that, so I did something like this:

initialize_test_class <- function(x) UseMethod("initialize_test_class")

initialize_test_class.default <- function(x) {
  warning("don't know what to do")
}

initialize_test_class.list <- function(x) {
  print("initialized with a list")
}

initialize_test_class.character <- function(x) {
  print("initialized with a character")
}

TestClass <- R6::R6Class(
  classname = "TestClass",
  public = list(
    initialize = function(x) {
      initialize_test_class(x) 
    }
  )
)


TestClass$new(list(a = 1))
TestClass$new("foo")

And it works, but I'm wondering maybe there's a more elegant way to do it? Some more general advices about OOP good practices in such cases are welcome, too.


Solution

  • If initialization of the R6 class is the sole purpose of your S3 class, I would say you are definitely over-engineering this. There are other methods for type-checking input that are simpler (and possibly safer) than relying on S3 dispatch. For example is.list(x) and is.character(x) might be useful here, or inherits(x, 'list') and inherits(x, 'character').

    So I think a more elegant solution here would be:

    TestClass <- R6::R6Class(
      classname = "TestClass",
      public = list(
        initialize = function(x) {
          if(is.list(x)) cat('initialized with a list\n') else 
          if(is.character(x)) cat('initialized with a character\n') else 
          cat("I don't know what to do\n")
        }
      )
    )
    

    Though depending on the complexity of your initialization methods, you may wish to bring the methods outside of the class definition, but don't have them as S3:

    init_list <- function(x) cat('initialized with a list\n')
    init_char <- function(x) cat('initialized with a character\n')
    init_none <- function(x) cat("I don't know what to do\n")
    
    TestClass <- R6::R6Class(
      classname = "TestClass",
      public = list(
        initialize = function(x) {
          if(is.list(x)) init_list(x) else 
          if(is.character(x)) init_char(x) else init_none(x)
        }
      )
    )
    

    Both of which give the same result:

    TestClass$new(list(a = 1))
    #> initialized with a list
    #> <TestClass>
    #>   Public:
    #>     clone: function (deep = FALSE) 
    #>     initialize: function (x)
    
    TestClass$new("foo")
    #> initialized with a character
    #> <TestClass>
    #>   Public:
    #>     clone: function (deep = FALSE) 
    #>     initialize: function (x)
    

    Created on 2023-09-21 with reprex v2.0.2