Skip to content

Commit 3566af0

Browse files
authored
Re-allow environments as parents (#622)
Now forbidding `S7_data()` and `convert()` instead. Fixes #590
1 parent 52512eb commit 3566af0

15 files changed

Lines changed: 215 additions & 32 deletions

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* `S7_error_method_not_found` now has a correct class vector without a duplicate `"error"` entry (@jjjermiah, #604).
88
* `method<-` now gives a clear error when assigning a primitive function (e.g. `log`) as a method (#608).
99
* `method<-` and `method()` now accept a length-1 list as `signature` for single-dispatch generics, matching the list-of-classes form required for multi-dispatch (#555).
10+
* `new_class()` experimentally allows `class_environment` as a parent again, so you can build S7 objects that share R's reference semantics for environments. This support is provisional: because environments are mutated in place, some operations behave differently than for value-typed S7 objects, and the API may change. `S7_data()` and `S7_data<-()` error on environment-based objects, since they would otherwise destroy the object's S7 attributes in place (#590).
1011
* `new_object()` now gives an informative error when `.parent` is a class specification rather than an instance of the parent class (#409).
1112
* `S7_inherits()` and `check_is_S7()` now accept any class specification (S7 class, S7 union, S3 class, S4 class, or base type wrapper like `class_integer`), not just S7 classes (#556).
1213
* Method dispatch on `class_missing` now correctly handles missing arguments forwarded through a wrapper functions (#595).

R/base-environment.R

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#' Use an environment as the base type of an S7 class
2+
#'
3+
#' @description
4+
#' \ifelse{html}{
5+
#' {\figure{lifecycle-experimental.svg}{options: alt='\[Experimental\]'}}}{\strong{\[Experimental\]
6+
#' }}
7+
#'
8+
#' `class_environment` is the [base][base_classes] wrapper for environments.
9+
#' Unlike all other R objects, environments have reference semantics, i.e., they
10+
#' are modified in place. It's not clear what all the implications of this are
11+
#' for S7, so we are marking the use of `class_environment` as experimental.
12+
#'
13+
#' Its use is subject to the following caveats:
14+
#'
15+
#' * [S7_data()] and `S7_data<-()` error, because swapping the underlying data
16+
#' would destroy the existing attributes.
17+
#'
18+
#' * The default [convert()] method errors when upcasting to an environment
19+
#' because stripping the subclass's properties would mutate `from` in place.
20+
#'
21+
#' @export
22+
#' @examples
23+
#' Counter <- new_class("Counter", class_environment)
24+
#' counter <- Counter()
25+
#' counter$n <- 0L
26+
#'
27+
#' # Reference semantics: `copy` and `counter` are the same object, so
28+
#' # mutating one is visible through the other.
29+
#' copy <- counter
30+
#' copy$n <- 10L
31+
#' counter$n
32+
class_environment <- NULL
33+
34+
check_not_environment <- function(object, fn) {
35+
if (!is.environment(object)) {
36+
return(invisible())
37+
}
38+
39+
msg <- paste_c(
40+
sprintf("Can't call `%s` on an environment.\n", fn),
41+
"See ?class_environment for details."
42+
)
43+
stop(msg, call. = FALSE)
44+
}
45+
46+
# Define onload to avoid dependencies between files
47+
on_load_define_environment <- function() {
48+
class_environment <<- new_base_class("environment")
49+
}

R/base.R

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ str.S7_base_class <- function(object, ..., nest.lev = 0) {
113113
#' * `class_name`
114114
#' * `class_call`
115115
#' * `class_function`
116-
#' * `class_environment` (can only be used for properties)
116+
#'
117+
#' See also [class_environment] which is documented separately due to the
118+
#' complexities introduced by their reference semantics.
117119
#'
118120
#' We also include three union types to model numerics, atomics, and vectors
119121
#' respectively:
@@ -201,12 +203,6 @@ class_call <- new_base_class("call")
201203
#' @order 1
202204
class_function <- new_base_class("function", "fun")
203205

204-
#' @export
205-
#' @rdname base_classes
206-
#' @format NULL
207-
#' @order 1
208-
class_environment <- new_base_class("environment")
209-
210206
#' @export
211207
#' @rdname base_classes
212208
#' @format NULL

R/class.R

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -250,10 +250,6 @@ check_can_inherit <- function(x, arg = deparse(substitute(x))) {
250250
)
251251
stop(msg, call. = FALSE)
252252
}
253-
254-
if (is_base_class(x) && x$class == "environment") {
255-
stop("Can't inherit from an environment.", call. = FALSE)
256-
}
257253
}
258254

259255
is_class <- function(x) inherits(x, "S7_class")
@@ -349,7 +345,10 @@ str.S7_object <- function(object, ..., nest.lev = 0) {
349345
cat(if (nest.lev > 0) " ")
350346
cat(obj_desc(object))
351347

352-
if (!is_S7_type(object)) {
348+
if (is.environment(object)) {
349+
# Can't use S7_data() with environments
350+
cat(" ", format.default(object), "\n", sep = "")
351+
} else if (!is_S7_type(object)) {
353352
if (!typeof(object) %in% c("numeric", "integer", "character", "double")) {
354353
cat(" ")
355354
}

R/convert.R

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ convert <- function(from, to, ...) {
9898
}
9999

100100
convert_up <- function(from, to) {
101+
check_not_environment(from, "convert()")
102+
101103
from_class <- S7_class(from)
102104
if (is.null(from_class)) {
103105
from_props <- character()

R/data.R

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#' S7_data(MyDF(data.frame(x = 1, y = 2)))
2727
S7_data <- function(object) {
2828
check_is_S7(object)
29+
check_not_environment(object, "S7_data()")
2930

3031
out <- zap_attr(object, c(prop_names(object), "class", "S7_class"))
3132

@@ -47,6 +48,9 @@ base_parent <- function(class) {
4748
#' @export
4849
#' @rdname S7_data
4950
`S7_data<-` <- function(object, check = TRUE, value) {
51+
check_is_S7(object)
52+
check_not_environment(object, "S7_data<-")
53+
5054
s7_attrs <- c(prop_names(object), "class", "S7_class")
5155
for (name in s7_attrs) {
5256
attr(value, name) <- attr(object, name, exact = TRUE)
@@ -57,7 +61,6 @@ base_parent <- function(class) {
5761
return(invisible(value))
5862
}
5963

60-
6164
zap_attr <- function(x, names) {
6265
for (name in names) {
6366
attr(x, name) <- NULL

R/zzz.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ methods::setOldClass(c("S7_method", "function", "S7_object"))
140140
.onLoad <- function(...) {
141141
activate_backward_compatiblility()
142142

143+
on_load_define_environment()
143144
on_load_define_S7_generic()
144145
on_load_define_S7_method()
145146
on_load_make_convert_generic()

_pkgdown.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ reference:
4949
classes, and base types. See `vignette("compatibility")` for more details.
5050
contents:
5151
- base_classes
52+
- class_environment
5253
- base_s3_classes
5354
- new_S3_class
5455
- S4_register

man/base_classes.Rd

Lines changed: 3 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/class_environment.Rd

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)