#' @title Constructor for "immutable" S3-class property
#'
#' @description
#' This method should allow any S3 object in R to become immutable by
#' intercepting `[<-`, `[[<-`, `$<-` and `c` during S3-method dispatch and
#' returning an error.
#'
#' Reverse with call to the `mutable` function.
#'
#' @details
#' The motivation for this class was to create pseudo-private slots in an R
#' S4 object by preventing mutation of those slots outside of the accessors
#' written for the class. It should behave as expected for R object which
#' operate with 'copy-on-modify' semantics, including most base R functions and
#' S3 objects.
#'
#' An environment was not suitable for this case due
#' to the 'copy-by-reference' semantics, such that normal R assignment, which
#' users assume makes a copy of the object, actually references the same
#' environment in both the original and copy of the object.
#'
#' WARNING: This implementation is unable to intercept modifications to a
#' `data.table` via the `set*` group of methods. This is because these methods
#' are not S3 generics and therefore no mechanism exists for hooking into them
#' to extend their functionality. In general, this helper class will only work
#' for objects with an S3 interface.
#'
#' @param object,x Any R object which uses S3 method dispatch
#'
#' @return The `object` with "immutable" prepended to its class attribute.
#'
#' @examples
#' immutable_list <- immutable(as.list(1:5))
#' class(immutable_list)
#' # errors during assignment operations
#' tryCatch({ immutable_list$new <- 1 }, error=print)
#'
#' @seealso
#' [`assignment-immutable`], [`setOps-immutable`]
#'
#' @md
#' @rdname immutable
#' @name immutable
#' @export
immutable <- function(object) {
if (isS4(object)) stop("Can only set immutability for base and S3 classes!")
# call mutable to prevent assigning immutable class twice
structure(mutable(object), class=c("immutable", attributes(object)$class))
}
# register the new S3 class, so it can be used in S4 method dispatch
#' @rdname immutable
#' @name immutable
#' @export
setOldClass("immutable")
#' @rdname immutable
#' @export
setClassUnion("immutable_list", c("immutable", "list"))
#' @title Check if an R object inherits from the immutable S3-class.
#'
#' @return `logical(1)` Does the object inherit from the "immutable" S3-class?
#'
#' @examples
#' immutable_list <- immutable(as.list(1:5))
#' is.immutable(immutable_list)
#'
#' @rdname immutable
#' @export
is.immutable <- function(object) {
is(object, "immutable")
}
#' @title Print method for objects inheriting from the "immutable" S3-class
#'
#' @param ... Fallthrough arguments to `print.default`.
#'
#' @return None, `invisible(NULL)`
#'
#' @rdname immutable
#' @md
#' @export
print.immutable <- function(x, ...) {
other_cls <- setdiff(attributes(x)$class, "immutable")
class(x) <- other_cls
cat("immutable", class(x), "\n")
print(x, ...)
}
#' @rdname immutable
#' @export
show.immutable <- function(x) print(x)
# -- Intercept subset and concatentate operations to return another "immutable"
#' @title Intercept concatenation for "immutable" class objects to return another "immutable" class object.
#'
#' @description
#' Ensures that `c` and `append` to an "immutable" class object return an
#' immutable class object.
#'
#' @param x An R object inheriting from the "immutable" S3-clas
#' @param ... Objects to concatenate to `x`.
#'
#' @return x with one or more values appended to it.
#'
#' @md
#' @export
c.immutable <- function(x, ...) {
new_obj <- NextMethod()
immutable(new_obj)
}
.immutable_emsg <- "Object is immutable! Use `mutable(object)` to return a mutable copy."
#' @name setOps-immutable
#' @rdname setOps-immutable
#'
#' @title Subset an immutable object, returning another immutable object.
#'
#' @param x An R object inheriting from the "immutable" S3-class.
#' @param ... Catch any additional parameters. Lets objects with arbitrary
#' dimensions be made immutable.
#'
#' @return An immutable subset of `x`.
#'
#' @examples
#' immut_mat <- immutable(matrix(1:100, 10, 10))
#' immut_mat[1:5, 1:5]
#'
#' @md
#' @aliases subset, [.immutable, [[.immutable, $.immutable
#' @export
subset.immutable <- function(x, ...) {
sub_obj <- NextMethod()
immutable(sub_obj)
}
#' @name [
#' @rdname setOps-immutable
#' @export
`[.immutable` <- function(x, ...) {
if (is.data.table(x)) {
dots <- substitute(alist(...))
j_expr <- dots[["j"]]
if (is.null(j_expr) && length(dots) > 2)
j_expr <- dots[[2 + 1]] # index plus one due to alist call
if (!is.null(j_expr))
j_txt <- deparse(j_expr)
if (grepl(":=|let[ ]*\\(|set[ ]*\\(", j_txt))
stop("This data.table is immutable! No assignment by reference ",
"allowed. Use `mutable(x)` to return a mutable copy.",
call.=FALSE)
}
sub_obj <- NextMethod()
immutable(sub_obj)
}
#' @name [[
#' @rdname setOps-immutable
#' @export
`[[.immutable` <- function(x, ...) {
sub_obj <- NextMethod()
immutable(sub_obj)
}
#' @name $
#' @rdname setOps-immutable
#' @export
`$.immutable` <- function(x, ...) {
sub_obj <- NextMethod()
immutable(sub_obj)
}
# -- Intercept assignment to prevent modification
#' @name assignment-immutable
#' @rdname assignment-immutable
#'
#' @title Intercept assignment operations for "immutable" S3 objects.
#'
#' @description
#' Prevents modification of objects labelled with the "immutable" S3-class by
#' intercepting assignment during S3-method dispatch and returning an error.
#'
#' @param object,x An R object inherting from the "immutable" S3-class.
#' @param ... Catch subset arguments for various dimensions.
#' @param value Not used.
#'
#' @return None, throws an error.
#'
#' @examples
#' immutable_df <- immutable(data.frame(a=1:5, b=letters[1:5]))
#' # return immutable data.frame
#' immutable_df[1:4, ]
#' # return immutable vector
#' immutable_df$a
#'
#' @md
#' @usage \\method{subset}{immutable}(object, ...) <- value
#' @aliases subset<-.immutable, [<-.immutable, [[<-.immutable, $<-.immubtale,
#' colnames<-.immutable, rownames<-.immutable, dimnames<-.immutable,
#' names<-.immutable
#' @export
`subset<-.immutable` <- function(object, ..., value) {
stop(.immutable_emsg, call.=FALSE)
}
#' @name [<-
#' @rdname assignment-immutable
#' @export
`[<-.immutable` <- function(object, ..., value) {
stop(.immutable_emsg, call.=FALSE)
}
#' @name [[<-
#' @rdname assignment-immutable
#' @export
`[[<-.immutable` <- function(object, ..., value) {
stop(.immutable_emsg, call.=FALSE)
}
#' @name $<-
#' @rdname assignment-immutable
#' @export
`$<-.immutable` <- function(object, ..., value) {
stop(.immutable_emsg, call.=FALSE)
}
#' @name names<-
#' @rdname assignment-immutable
#' @export
`names<-.immutable` <- function(x, value)
stop(.immutable_emsg, call.=FALSE)
#' @name dimnames<-
#' @rdname assignment-immutable
#' @export
`dimnames<-.immutable` <- function(x, value)
stop(.immutable_emsg, call.=FALSE)
#' @name colnames<-
#' @rdname assignment-immutable
#' @usage \\method{colnames}{immutable}(x) <- value
#' @export
`colnames<-.immutable` <- function(x, value)
stop(.immutable_emsg, call.=FALSE)
#' @name rownames<-
#' @rdname assignment-immutable
#' @usage \\method{rownames}{immutable}(x) <- value
#' @export
`rownames<-.immutable` <- function(x, value)
stop(.immutable_emsg, call.=FALSE)
# -- Remove immutability from an R object
#' @title Remove the "immutable" S3-class from an R object, allowing it to be
#' modified normally again.
#'
#' @param object An R object inheriting from the "immutable" class.
#'
#' @return The `object` with the "immutable" class stripped from it.
#'
#' @examples
#' immut_list <- immutable(list())
#' mutable(immut_list)
#'
#' @md
#' @export
mutable <- function(object) UseMethod("mutable", object)
#'
#' @md
#' @importFrom data.table copy
#' @export
mutable.default <- function(object) {
new_class <- setdiff(attributes(object)$class, "immutable")
structure(copy(object), class=new_class)
}
# -- Make comparisons work for immutable objects
#' @export
Ops.immutable <- function(e1, e2) {
get(.Generic)(mutable(e1), mutable(e2))
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.