Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* `method<-` now gives a clear error when assigning a primitive function (e.g. `log`) as a method (#608).
* `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).
* `new_object()` now names its first argument `_parent` to minimise the chance of a clash with a property (#423). It also accepts a single unnamed named list as a shortcut for splicing property values, making it easier to programmatically construct an object from a list of properties (#497).
* `new_object()` no longer copies an S7 class each time a default or custom constructor creates an object. New objects instead store a shared internal class reference, which also preserves sharing when multiple objects are serialised together. Constructors created by older versions of S7 continue to work through the previous fallback (#742).
* `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).
* `method<-` no longer emits an "Overwriting method" message when re-registering an identical method, eliminating spurious messages from `devtools::load_all()` (#474).
* `new_class()` now errors if a child class overrides a parent property with a type that doesn't extend the parent's type, since such a class could never be instantiated (#352, #708).
Expand Down Expand Up @@ -57,6 +58,7 @@
* `set_props()` now names its first argument `_object` to minimise the chances of a clash with a property (#423). It also accepts a single unnamed named list as a shortcut for splicing property values, making it easier to set properties programmatically (#497).
* `str()` on S7 objects that inherit from data.frame (or other S3 classes whose underlying data has a `dim` attribute incompatible with the bare base type) no longer errors (#494).
* `super()` now works with S3 and S4 objects, not just S7 objects (#500).
* `validate()` now checks property types substantially faster, because a property restricted to a base type (e.g. `class_double`) no longer has its underlying type checked twice. Constructing an object with 50 base type properties is 1.6x faster, and with 10 base type properties 1.4x faster (#723).
* `validate()` now signals validation errors with class `S7_error_validation_failed`, so they can be caught with `tryCatch()` (#602, #605).

# S7 0.2.2
Expand Down
137 changes: 122 additions & 15 deletions R/class.R
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
#' argument for each property.
#'
#' A custom constructor should call `new_object()` to create the S7 object.
#' The first argument, `.data`, should be an instance of the parent class
#' (if used). The subsequent arguments are used to set the properties.
#' `new_class()` automatically associates a custom constructor with its class,
#' so no additional class argument is needed. The first argument to
#' `new_object()`, `_parent`, should be an instance of the parent class (if
#' used). The subsequent arguments are used to set the properties.
#' @param validator A function taking a single argument, `self`, the object
#' to validate.
#'
Expand Down Expand Up @@ -174,6 +176,12 @@ new_class <- function(
)
}

class_ref <- new.env(parent = emptyenv())
class(class_ref) <- "S7_class_ref"
constructor_env <- new.env(parent = environment(constructor))
constructor_env$.S7_class_ref <- class_ref
environment(constructor) <- constructor_env

object <- constructor
# A class's metadata is stored as plain attributes on the class object.
# Must synchronise with prop_names().
Expand All @@ -192,6 +200,13 @@ new_class <- function(
attr(object, "S7_class_name") <- class_name
attr(object, "S7_dispatch") <- S7_class_dispatch(class_name, parent_resolved)
class(object) <- c("S7_class", "S7_object")
class_ref$class <- object
class_ref$construction <- new_construction_metadata(
parent = parent_resolved,
properties = all_props,
new_properties = new_props,
validator = validator
)

if (S7_extends_S4(object)) {
S4_register_subclass(object, env = parent.frame())
Expand All @@ -200,6 +215,72 @@ new_class <- function(
global_variables(names(all_props))
object
}

new_construction_metadata <- function(
parent,
properties,
new_properties,
validator
) {
stored <- properties[vlapply(properties, \(x) is.null(x$getter))]
setter_names <- names(properties)[vlapply(properties, prop_has_setter)]
storage_names <- prop_storage_names_r(names(properties))
stored_storage_names <- prop_storage_names_r(names(stored))

base_types <- lapply(stored, function(prop) {
if (is_base_class(prop$class) && is.null(prop$validator)) {
prop$class$class
} else {
NULL
}
})

list(
setter_names = setter_names,
storage_names = setNames(storage_names, names(properties)),
parent_property_names = names(class_properties(parent)),
validation_properties = stored,
validation_storage_names = stored_storage_names,
validation_base_types = base_types,
direct_property_access = !is_S4_class(parent) &&
!(is_class(parent) && S7_extends_S4(parent)),
validates_nothing = length(new_properties) == 0 &&
is.null(validator)
)
}

prop_storage_names_r <- function(names) {
special <- c(
names = "_names",
dim = "_dim",
dimnames = "_dimnames",
class = "_class",
tsp = "_tsp",
comment = "_comment",
row.names = "_row_names"
)
replace <- match(names, names(special), nomatch = 0L)
names[replace > 0L] <- unname(special[replace])
names
}

class_construction_metadata <- function(class) {
class_storage <- S7_class_storage(class)
metadata <- if (inherits(class_storage, "S7_class_ref")) {
class_storage$construction
}
if (!is.null(metadata)) {
return(metadata)
}

properties <- attr(class, "properties", TRUE)
new_construction_metadata(
parent = attr(class, "parent", TRUE),
properties = properties,
new_properties = properties,
validator = attr(class, "validator", TRUE)
)
}
globalVariables(c(
"name",
"parent",
Expand Down Expand Up @@ -384,15 +465,25 @@ check_parent <- function(parent, class, call = sys.call(-1L)) {
#' @rdname new_class
#' @export
new_object <- function(`_parent`, ...) {
class <- sys.function(sys.parent())
class_ref <- get0(
".S7_class_ref",
envir = parent.frame(),
inherits = TRUE,
ifnotfound = NULL
)
if (inherits(class_ref, "S7_class_ref")) {
class <- class_ref$class
} else {
class <- sys.function(sys.parent())
}
if (!inherits(class, "S7_class")) {
stop2("`new_object()` must be called from within a constructor.")
}
# This is the hottest function in S7, so read the class metadata we need once,
# up front.
class_abstract <- attr(class, "abstract", TRUE)
class_props <- attr(class, "properties", TRUE)
class_parent <- attr(class, "parent", TRUE)
metadata <- class_construction_metadata(class)

if (class_abstract && !is_constructing_parent_part(class)) {
msg <- sprintf(
Expand All @@ -409,15 +500,18 @@ new_object <- function(`_parent`, ...) {

args <- collect_dots(...)

has_setter <- vlapply(class_props[names(args)], prop_has_setter)
has_setter <- names(args) %in% metadata$setter_names
self_attrs <- args[!has_setter]
names(self_attrs) <- prop_storage_rename(names(self_attrs))
names(self_attrs) <- metadata$storage_names[names(self_attrs)]

# We must awkwardly operate on `_parent` rather than binding to a local
# variable; since otherwise the extra binding causes ALTREP-wrapped values to
# be materialised when byte-compiled (#607).
attrs <- c(
list(class = class_dispatch(class), `_S7_class` = class),
list(
class = class_dispatch(class),
`_S7_class` = if (S7_extends_S4(class)) class else class_ref %||% class
),
self_attrs,
attributes(`_parent`)
)
Expand All @@ -436,15 +530,19 @@ new_object <- function(`_parent`, ...) {
inherits(class_parent, "S7_object") &&
!attr(class_parent, "abstract", TRUE)
parent_props_reset <- parent_validated &&
any(
names2(args) %in% names2(attr(class_parent, "properties", TRUE))
any(names2(args) %in% metadata$parent_property_names)
if (
!metadata$validates_nothing ||
!parent_validated ||
parent_props_reset
) {
validate_from(
`_parent`,
parent = if (parent_validated && !parent_props_reset) class_parent,
# Attribute validation failures to the constructor call, not new_object()
call = sys.call(-1L)
)
validate_from(
`_parent`,
parent = if (parent_validated && !parent_props_reset) class_parent,
# Attribute validation failures to the constructor call, not new_object()
call = sys.call(-1L)
)
}

`_parent`
}
Expand Down Expand Up @@ -514,6 +612,15 @@ S7_class <- function(object) {
)
}

S7_class_storage <- function(class) {
get0(
".S7_class_ref",
envir = environment(class),
inherits = TRUE,
ifnotfound = class
)
}


check_prop_names <- function(properties, call = sys.call(-1L)) {
nms <- names2(properties)
Expand Down
2 changes: 1 addition & 1 deletion R/convert.R
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ convert_up <- function(from, to, call = sys.call(-1L)) {
}

from <- zap_attr(from, c(setdiff(from_props, to_props), "S7_class"))
attr(from, "_S7_class") <- to
attr(from, "_S7_class") <- if (isS4(from)) to else S7_class_storage(to)
class(from) <- class_dispatch(to)
} else if (is_S4_coerce(from, to)) {
from <- convert_S4(from, to)
Expand Down
16 changes: 11 additions & 5 deletions R/property.R
Original file line number Diff line number Diff line change
Expand Up @@ -304,18 +304,24 @@ signal_setter_error <- function(value, object, name) {

# called from src/prop.c
prop_validate <- function(prop, value, object = NULL) {
if (!class_inherits(value, prop$class)) {
class <- prop$class

if (!class_inherits(value, class)) {
return(sprintf(
"%s must be %s, not %s",
prop_label(object, prop$name),
class_desc(prop$class),
class_desc(class),
obj_desc(value)
))
}

class_error <- class_validate(prop$class, value)
if (length(class_error) > 0) {
return(paste0(prop_label(object, prop$name), ": ", class_error))
# A base class's validator does nothing but re-check the underlying type,
# which `class_inherits()` has just done.
if (!is_base_class(class)) {
class_error <- class_validate(class, value)
if (length(class_error) > 0) {
return(paste0(prop_label(object, prop$name), ": ", class_error))
}
}

if (is.null(validator <- prop$validator)) {
Expand Down
4 changes: 4 additions & 0 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ global_variables <- function(names) {
assign(".__global__", current, envir = env)
}

obj_addr <- function(x) {
.Call(obj_addr_, x)
}

vlapply <- function(X, FUN, ...) {
vapply(X = X, FUN = FUN, FUN.VALUE = logical(1), ...)
}
Expand Down
30 changes: 23 additions & 7 deletions R/valid.R
Original file line number Diff line number Diff line change
Expand Up @@ -152,23 +152,39 @@ validate_from <- function(
}

validate_properties <- function(object, class, parent_class = NULL) {
errors <- character()
metadata <- class_construction_metadata(class)
props <- metadata$validation_properties
if (length(props) == 0) {
return(character())
}

# runs on every construction
parent_props <- if (is_class(parent_class)) {
attr(parent_class, "properties", TRUE)
}
errors <- character()

for (prop_obj in attr(class, "properties", TRUE)) {
# Don't validate dynamic properties
if (!is.null(prop_obj$getter)) {
for (i in seq_along(props)) {
prop_obj <- props[[i]]
name <- prop_obj$name
# Skip properties inherited unchanged from an already-validated parent
if (!is.null(parent_props) && identical(parent_props[[name]], prop_obj)) {
next
}
# Skip properties inherited unchanged from an already-validated parent
if (identical(parent_props[[prop_obj$name]], prop_obj)) {

value <- if (metadata$direct_property_access && !isS4(object)) {
attr(object, metadata$validation_storage_names[[i]], exact = TRUE)
} else {
prop(object, name)
}

# The common case: a base type property, already the right type, with no
# validator of its own. Nothing for prop_validate() to find.
base_type <- metadata$validation_base_types[[i]]
if (!is.null(base_type) && base_type == base_class(value)) {
next
}

value <- prop(object, prop_obj$name)
errors <- c(errors, prop_validate(prop_obj, value))
}

Expand Down
6 changes: 4 additions & 2 deletions man/new_class.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/init.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ extern SEXP prop_set_(SEXP, SEXP, SEXP, SEXP);
extern SEXP prop_storage_rename_(SEXP);
extern SEXP S7_eval_bare_(SEXP, SEXP);
extern SEXP class_type_(SEXP);
extern SEXP obj_addr_(SEXP);
extern void prop_init(void);
extern void class_type_init(void);

Expand All @@ -27,6 +28,7 @@ static const R_CallMethodDef CallEntries[] = {
CALLDEF(prop_storage_rename_, 1),
CALLDEF(S7_eval_bare_, 2),
CALLDEF(class_type_, 1),
CALLDEF(obj_addr_, 1),
{NULL, NULL, 0}
};

Expand All @@ -38,6 +40,7 @@ static const R_ExternalMethodDef ExternalEntries[] = {
SEXP sym_ANY;
SEXP sym_S7_class;
SEXP sym_S7_class_legacy;
SEXP sym_class;

SEXP sym_name;
SEXP sym_parent;
Expand Down Expand Up @@ -102,6 +105,7 @@ void R_init_S7(DllInfo *dll)
sym_S7_class = Rf_install("_S7_class");
// Legacy name used by objects created with an older version of S7.
sym_S7_class_legacy = Rf_install("S7_class");
sym_class = Rf_install("class");
sym_name = Rf_install("name");
sym_parent = Rf_install("parent");
sym_package = Rf_install("package");
Expand Down
10 changes: 10 additions & 0 deletions src/prop.c
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#include "compat.h"
#include <stdio.h>
#include <string.h>

extern SEXP sym_S7_class;
extern SEXP sym_S7_class_legacy;
extern SEXP sym_class;

extern SEXP sym_name;
extern SEXP sym_parent;
Expand Down Expand Up @@ -40,6 +42,8 @@ SEXP get_S7_class(SEXP object) {
SEXP S7_class = Rf_getAttrib(object, sym_S7_class);
if (S7_class == R_NilValue)
S7_class = Rf_getAttrib(object, sym_S7_class_legacy);
if (TYPEOF(S7_class) == ENVSXP && Rf_inherits(S7_class, "S7_class_ref"))
S7_class = s7_get_var_in_frame(S7_class, sym_class, R_NilValue);
return S7_class;
}

Expand All @@ -48,6 +52,12 @@ SEXP S7_class_(SEXP object) {
return get_S7_class(object);
}

SEXP obj_addr_(SEXP object) {
char address[2 * sizeof(void *) + 3];
snprintf(address, sizeof(address), "%p", (void *) object);
return Rf_mkString(address);
}

static inline
SEXP eval_here(SEXP lang) {
PROTECT(lang);
Expand Down
Loading
Loading