Question

Is there a way to prevent modification of an S4 object in R?

I want to create an S4 class that is a subclass of another S4 class, with additional constraints (like the object should be sorted). Given that modifications of the object could result in violations of the constraints, I would like to make the object immutable. Is there a way to do this? Perhaps call validObject() after a method is called.

 4  102  4
1 Jan 1970

Solution

 1

My approach is to create a secured environment (called SEnv below) where all objects are checked by validObject before entering into this secured environment. Then, anytime you take out any objects from this secured environment, you can be sure they are valid objects without rechecking them.

setClass("SEnv", contains = "environment")

setMethod(
    "[[<-",
    signature(x = "SEnv", i = "character", j = "missing", value = "ANY"),
    function(x, i, j, ..., value) {
        validObject(value, complete = TRUE)
        ev <- as(x, "environment")
        ev[[i]] <- value
        lockBinding(i, ev)
        x
    }
)

setMethod(
    "$<-",
    signature(x = "SEnv", value = "ANY"),
    function(x, name, value) {
        validObject(value, complete = TRUE)
        ev <- as(x, "environment")
        ev[[name]] <- value
        lockBinding(name, ev)
        x
    }
)

Example

  1. Let's define a C1 class with 2 slots: x as integer and y as character. We also add a constraint that x and y must have the same length.
setClass("C1", slots = c(x = "integer", y = "character"))
setValidity("C1", function(object) {
    res <- rep(NA_character_, 1)
    if (length(object@x) != length(object@y)) res[[1]] <- "length of slot x and y must equal"
    res <- c(na.omit(res))
    res <- if (length(res)) res else TRUE
    res
})
  1. The C2 class extends the C1 class and adds an additional constraint which requires slot x to be sorted.
setClass("C2", contains = c("C1"))
setValidity("C2", function(object) {
    res <- rep(NA_character_, 1)
    if (is.unsorted(object@x) && is.unsorted(rev(object@x))) res[[1]] <- "slot x is not sorted"
    res <- c(na.omit(res))
    res <- if (length(res)) res else TRUE
    res
})
  1. Test drive
ee <- new("SEnv")
ee[["o1"]] <- new("C2", x = c(10L:1L), y = rep("A", 10))
ee$o2 <- 1:5 # this is a valid integer object
ee$o2 <- 6:10 # object `o2` is locked and cannot change

# Try to create a malformed `C2` object 
o_bad <- ee$o1
o_bad@x <- c(5L, 6L, 7L)
o_bad # Even though the object is a malformed `C2`, it is still printed as "An object of class "C2"" and method dispatch through signature `C2`.
#An object of class "C2"
#Slot "x":
#[1] 5 6 7
#Slot "y":
# [1] "A" "A" "A" "A" "A" "A" "A" "A" "A" "A"

ee$o3 <- o_bad # this bad object cannot enter our secured environment due to failure of validity method of its superclass `C1`
# Error in validObject(value, complete = TRUE) : 
#  invalid class “C2” object: length of slot x and y must equal
2024-07-15
Hieu Nguyen