The default constructor's inheritance of parent constructor formals seems unsafe, since the evaluation environment might change. I encountered this when developing an S7 class in a package with a custom constructor, and then extending this class in a different package with the default constructor.
Perhaps passing parent properties to the parent constructor via ... is a safer alternative? This would also fix #467 and #585.
Explanatory MRE:
The built-in constructor evaluates default arguments.
e <- new.env()
local({
default <- 1L
parent <- new_class(
"parent",
properties = list(
x = new_property(class_integer, default = default)
)
)
}, envir = e)
# evaluated property defaults are used in formals
formals(e$parent)
#> $x
#> [1] 1
e$parent()
#> <parent>
#> @ x: int 1
Suppose we prefer the constructor's formals to maintain the symbol defaults
# Custom constructor for unevaluated defaults
local({
default <- 1L
parent <- new_class(
"parent",
properties = list(
x = new_property(class_integer, default = default)
),
constructor = function(x = default) {
new_object(S7::S7_object(), x = x)
}
)
}, envir = e)
# Constructor with symbol property defaults used in formals
formals(e$parent)
#> $x
#> default
e$parent()
#> <parent>
#> @ x: int 1
This works, the symbols are found from the correct environment when needed.
Then we inherit this class as a parent from a class in a different environment.
child <- new_class(
"child",
e$parent,
properties = list(
y = new_property(class_integer, default = 0L)
)
)
# Parent constructor formals are copied to child constructor
# But the environment is now different, so the symbols cannot be found
formals(child)
#> $x
#> default
#>
#> $y
#> [1] 0
child()
#> Error in `child()`:
#> ! object 'default' not found
The parent constructor's formals are merged with the child constructor's formals, and evaluated in the child constructor's environment -- so the symbol cannot be found.
If we instead pass through parent properties to the parent constructor via ..., the appropriate arguments are evaluated in the appropriate environment.
# Alternatively, passing ... to parent methods evaluates formals in the
# appropriate context/environment.
child <- new_class(
"child",
e$parent,
properties = list(
y = new_property(class_integer, default = 0L)
),
constructor = function(..., y = 0L) {
new_object(e$parent(...), y = y)
}
)
formals(child)
#> $...
#>
#>
#> $y
#> [1] 0
child()
#> <child>
#> @ x: int 1
#> @ y: int 0
Created on 2026-05-06 with reprex v2.1.1
The default constructor's inheritance of parent constructor formals seems unsafe, since the evaluation environment might change. I encountered this when developing an S7 class in a package with a custom constructor, and then extending this class in a different package with the default constructor.
Perhaps passing parent properties to the parent constructor via
...is a safer alternative? This would also fix #467 and #585.Explanatory MRE:
library(S7)The built-in constructor evaluates default arguments.
Suppose we prefer the constructor's formals to maintain the symbol defaults
This works, the symbols are found from the correct environment when needed.
Then we inherit this class as a parent from a class in a different environment.
The parent constructor's formals are merged with the child constructor's formals, and evaluated in the child constructor's environment -- so the symbol cannot be found.
If we instead pass through parent properties to the parent constructor via
..., the appropriate arguments are evaluated in the appropriate environment.Created on 2026-05-06 with reprex v2.1.1