Search code examples
rr-s4slot

R: How to truly remove an S4 slot from an S4 object (Solution attached!)


Let's say I define an S4 class 'foo' with two slots 'a' and 'b', and define an object x of class 'foo',

setClass(Class = 'foo', slots = c(
  a = 'numeric',
  b = 'character'
))
x <- new('foo', a = rnorm(1e3L), b = rep('A', times = 1e3L))
format(object.size(x), units = 'auto') # "16.5 Kb"

Then I want to remove slot 'a' from the definition of 'foo'

setClass(Class = 'foo', slots = c(
  b = 'character'
))
slotNames(x) # slot 'a' automatically removed!! wow!!!

I see that R automatically take cares my object x and have the slot 'a' removed. Nice! But wait, the size of object x is not reduced.

format(object.size(x), units = 'auto') # still "16.5 Kb"
format(object.size(new(Class = 'foo', x)), units = 'auto') # still "16.5 Kb"

Right.. Somehow 'a' is still there but I just cannot do anything to it

head(x@a) # `'a'` is still there
rm(x@a) # error
x@a <- NULL # error

So question: how can I really remove slot 'a' from x and have its size reduced (which is my primary concern)?


My deepest gratitude to all answers!

The following solution is inspired by dww

trimS4slot <- function(x) {
  nm0 <- names(attributes(x))
  nm1 <- names(getClassDef(class(x))@slots) # ?methods::.slotNames
  if (any(id <- is.na(match(nm0, table = c(nm1, 'class'))))) attributes(x)[nm0[id]] <- NULL  # ?base::setdiff
  return(x)
}
format(object.size(y1 <- trimS4slot(x)), units = 'auto') # "8.5 Kb"

The following solution is inspired by Robert Hijmans

setClass('foo1', contains = 'foo')
format(object.size(y2 <- as(x, 'foo1')), units = 'auto') # "8.5 Kb"

method::as probably does some comprehensive checks, so it's quite slow though

library(microbenchmark)
microbenchmark(trimS4slot(x), as(x, 'foo1')) # ?methods::as 10 times slower

Solution

  • What @dww suggests is nifty, and answers your question. But isn't the point of a class that you are guaranteed that its members (the slots) will always be there? If you don't care about that you can use the anything goes S3 classes instead? With S4, I would suggest a more formal approach like this:

    setClass(Class = 'bar', slots = c(b = 'character'))
    
    setClass(Class = 'foo', contains='bar', slots = c(a = 'numeric'))
    
    x <- new('foo', a = rnorm(1e3L), b = rep('A', times = 1e3L))
    format(object.size(x), units = 'auto')
    #[1] "16.5 Kb"
    
    x <- as(x, "bar")  
    format(object.size(x), units = 'auto')
    #[1] "8.5 Kb"
    

    And if this is just about size, why not just do

    x <- new('foo', a = rnorm(1e3L), b = rep('A', times = 1e3L))
    x@b <- ""
    format(object.size(x), units = 'auto')
    #[1] "8.7 Kb"
    

    To me this is clearly the best solution because of its simplicity.