Search code examples
rexpressionsubsetr6

Use an expression to subset R6Class based on attributes


I have a R6 class BarContainer that contains a vector of Bar, where Bar is another R6 class. I would like to implemented a method subset in BarContainer that would subset the vector of Bar to match with the expression given as argument.

Bar = R6::R6Class(
    ### Class name
    "Bar",

    ### Public members
    public = list(
        ### Attributes
        a = -1,
        b = -1,
        c = -1,

        ### Initialize
        initialize = function(A, B, C)
        {
            self$a = A
            self$b = B
            self$c = C
        }
    )
)

BarContainer = R6::R6Class(
    ### Class name
    "BarContainer",

    ### Public members
    public = list(
        ### Attributes
        self$bars = c(),

        ### Initialize
        initialize = function(input)
        {
            bars = input
        }#,

        ### Subset
        subset = function(expr)
        {
            # ??? self[eval(parse(text=expr))] ???
        }
    )
)

Here is a possible use case for subset

mySubsettedContainer = myContainer$subset(a == 3 && (b > 0 || c > 0) )

How could this be implemented? Any other solution (eventually using several expressions or something else) to implement this subset method?

Note that Bar has a whole bunch of helpful methods and this oop approach really clean things up. I would like to avoid keep my Bar objects as rows of a data.frame (e.g. bars = data.frame(a = .., b = .., c = ..)) even if it would make the implementation of a subset method in BarContainer much easier.


Solution

  • If you want BarContainer$subset to work in this way, you will need to capture the expression and evaluate it for each Bar in BarContainer to get a logical vector, then use this to subset the bars field in your container. The resulting list of Bar objects can then be passed to a BarContainer$new call to return a new BarContainer with the subsetted Bar objects.

    You can capture the expression with match.call() and evaluate it in the context of each Bar using an lapply, though you need to represent each Bar as a list within the lapply for it to be a valid evaluation context. That means the implementation would look something like this:

    BarContainer = R6::R6Class(
      ### Class name
      "BarContainer",
      
      ### Public members
      public = list(
        ### Attributes
        bars = c(),
        
        ### Initialize
        initialize = function(input)
        {
          self$bars = input
        },
        
        ### Subset
        subset = function(expr)
        {
          ex <- as.list(match.call()[-1])$exp
          ss <- sapply(self$bars, function(x){
            eval(ex, envir = as.list(x), enclos = parent.frame())})
          return(BarContainer$new(self$bars[ss]))
        }
      )
    )
    

    So we can create an example set-up like this:

    # Create 4 Bars for our container:
    bar1 <- Bar$new(1, 1, 1)
    bar2 <- Bar$new(2, 2, 2)
    bar3 <- Bar$new(3, 3, 3)
    bar4 <- Bar$new(4, 4, 4)
    
    # Populate our container:
    bar_container <- BarContainer$new(c(bar1, bar2, bar3, bar4))
    

    And our subset function works as expected:

    subsetted_container <- bar_container$subset(a > 1 & c < 4)
    
    subsetted_container$bars
    #> [[1]]
    #> <Bar>
    #>   Public:
    #>     a: 2
    #>     b: 2
    #>     c: 2
    #>     clone: function (deep = FALSE) 
    #>     initialize: function (A, B, C) 
    #> 
    #> [[2]]
    #> <Bar>
    #>   Public:
    #>     a: 3
    #>     b: 3
    #>     c: 3
    #>     clone: function (deep = FALSE) 
    #>     initialize: function (A, B, C) 
    

    Remember, the subsetted BarContainer won't hold copies of the original Bar objects, but pointers to the objects themselves. So if you alter the first Bar within the subsetted container, it will alter bar2 and the second Bar in the non-subsetted container. I'm guessing this is the desired behaviour anyway, but if not you will have to deep clone each Bar as part of your subsetting method.