diff --git a/NEWS.md b/NEWS.md index 03ace2a7..ee6f4722 100644 --- a/NEWS.md +++ b/NEWS.md @@ -18,7 +18,7 @@ * `method<-` can now register methods on S3 and S4 generics with base types (e.g. `class_character`), S3 classes (`new_S3_class()`, `class_factor`, etc.), S7 unions (expanded to one registration per class), `class_any` (registered as the `default` method), and `NULL` (registered as the `NULL` method) (#455). * `new_class()` now allows properties named `names`, `dim`, `dimnames`, `class`, `comment`, `tsp`, and `row.names`. But property names beginning with `_` are now reserved for internal use (#579). * `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). -* `new_external_class()` creates a delayed reference to an S7 class in another package (or your own package, but not yet defined). It is useful for registering methods on classes from suggested packages (#573) and for creating self-referential or mutually recursive classes (#250). +* `new_external_class()` creates a delayed reference to an S7 class in another package (or your own package, but not yet defined). It is useful for registering methods on classes from suggested packages (#573), for creating self-referential or mutually recursive classes (#250), and for subclassing a class from a soft dependency (the parent is only resolved when an object is constructed, so your package builds and loads even if the parent's package is absent). * `new_object()` now gives an informative error when `.parent` is a class specification rather than an instance of the parent class (#409). * `new_object()` no longer materialises ALTREP parent values (e.g. `seq_len()`), so constructing an S7 object that wraps a large compact integer sequence is now O(1) in memory instead of O(n) (@kschaubroeck, #607). * `new_object()` no longer re-runs property validators for properties inherited unchanged from an already-validated parent class, so constructing an instance of a deeply nested class hierarchy validates each property exactly once (#539). diff --git a/R/class.R b/R/class.R index 7bfa02c1..df5a8afa 100644 --- a/R/class.R +++ b/R/class.R @@ -248,7 +248,9 @@ c.S7_class <- function(...) { stop2("Can not combine S7 class objects.") } -can_inherit <- function(x) is_base_class(x) || is_S3_class(x) || is_class(x) +can_inherit <- function(x) { + is_base_class(x) || is_S3_class(x) || is_class(x) || is_external_class(x) +} check_can_inherit <- function( x, diff --git a/R/constructor.R b/R/constructor.R index 83cc88f7..dbc4c49a 100644 --- a/R/constructor.R +++ b/R/constructor.R @@ -5,6 +5,11 @@ new_constructor <- function( package = NULL ) { properties <- as_properties(properties) + + if (is_external_class(parent)) { + return(new_external_constructor(parent, properties, envir, package)) + } + arg_info <- constructor_args(parent, properties, envir, package) self_args <- as_names(names(arg_info$self), named = TRUE) @@ -97,6 +102,57 @@ constructor_args <- function( list(parent = parent_args, self = self_args) } +# Constructor for a class that inherits from an external class. The parent's +# package might not be loaded when the class is defined, so rather than inlining +# the parent's constructor arguments (which would require resolving the external +# class now), the constructor takes the child's own properties plus a `...` that +# is forwarded to the parent. The external class is only resolved the first time +# an object is constructed (when its package must be loaded anyway): at that +# point we rebuild the class with the resolved parent so that inherited +# properties, validation, and dispatch all behave as if the parent had been +# known up front. The completed class is cached for subsequent calls. +new_external_constructor <- function(parent, properties, envir, package) { + properties <- properties[!vlapply(properties, prop_is_read_only)] + self_args <- as.pairlist(lapply( + setNames(, names2(properties)), + function(name) prop_default(properties[[name]], envir, package) + )) + self_names <- as_names(names(self_args), named = TRUE) + + # Bind the resolver into the constructor's environment: the wrapper runs in + # the consumer's namespace, which only imports S7's *exported* functions, so + # this internal helper must be reachable lexically. + env <- new.env(parent = envir) + env$parent <- parent + env$completed <- NULL + env$complete_external_class <- complete_external_class + + body <- bquote( + { + if (is.null(completed)) { + completed <<- complete_external_class(sys.function(), parent) + } + completed(..(self_names), ...) + }, + splice = TRUE + ) + + new_function(c(self_args, alist(... = )), body, env) +} + +# Rebuild a class that inherits from an external `parent`, now that the parent's +# package is loaded and the external class can be resolved. +complete_external_class <- function(child, parent) { + new_class( + child@name, + parent = resolve_external_class_req(parent), + package = child@package, + properties = child@properties, + abstract = child@abstract, + validator = child@validator + ) +} + # helpers ----------------------------------------------------------------- diff --git a/R/external-class.R b/R/external-class.R index ce7d2555..8b043c24 100644 --- a/R/external-class.R +++ b/R/external-class.R @@ -25,6 +25,15 @@ #' new_class("tree", properties = list(child = NULL | tree_stub)) #' ``` #' +#' * To subclass a class from a soft dependency. The child constructor forwards +#' `...` to the parent, so the parent is only resolved when an object is +#' constructed (not when the class is defined), and your package builds and +#' loads even if the parent's package is absent. +#' +#' ```R +#' Child <- new_class("Child", parent = new_external_class("pkg", "Parent")) +#' ``` +#' #' Make sure to call [S7_on_load()] in your package's `.onLoad()` so that #' deferred method registrations fire when the relevant package is loaded. #' diff --git a/man/new_external_class.Rd b/man/new_external_class.Rd index b1816605..d9563e37 100644 --- a/man/new_external_class.Rd +++ b/man/new_external_class.Rd @@ -38,6 +38,13 @@ self-referential or mutually recursive class. \if{html}{\out{