Skip to content

Improve construction of subclasses#694

Open
hadley wants to merge 12 commits into
mainfrom
parent-props
Open

Improve construction of subclasses#694
hadley wants to merge 12 commits into
mainfrom
parent-props

Conversation

@hadley

@hadley hadley commented Jun 10, 2026

Copy link
Copy Markdown
Member

hadley added 2 commits June 10, 2026 16:49
* `constructor_args()` no longer drops child properties that shadow parent properties, so they become constructor args with the child's default. Fixes #467.
* The generated body no longer passes declared properties to the parent constructor; they go to `new_object()` instead, which runs the child's setter and class check. Fixes #585.
* Read-only properties are removed from the constructor.
* Constructors for subclasses of abstract classes now include parent properties.
Comment thread R/class.R
}
}

# Combine properties from parent, overriding as needed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This manipulation now happens in new_constructor() so it has both the parent and overridden properties.

new_constructor(foo1, list())
Output
function ()
function (x = numeric(0))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a clear bug that we never noticed before.

@hadley hadley requested a review from t-kalinowski June 15, 2026 22:23
Comment thread R/constructor.R Outdated
# Declared properties (including overrides of parent properties) are set by
# new_object() so that their defaults and setters are respected;
# only the remaining arguments are passed to the parent constructor.
parent_arg_nms <- setdiff(names(arg_info$parent), names2(properties))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implicitly requires that overridden properties are optional in the parent.
E.g., with this approach, this fails:

  devtools::load_all()
#> ℹ Loading S7
  foo <- new_class(
    "foo",
    properties = list(
      x = new_property(class_numeric, default = quote(stop("need x")))
    )
  )
  foo2 <- new_class(
    "foo2",
    parent = foo,
    properties = list(
      x = new_property(class_numeric, default = 2)
    )
  )
  
  `attributes<-`(foo2, NULL)
#> function (x = 2) 
#> S7::new_object(foo(), x = x)
#> <environment: 0x7698baa50>
  
  foo2()
#> Error in `foo()`:
#> ! need x
  foo2(x = 3)
#> Error in `foo()`:
#> ! need x

Is this what we want?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that doesn't seem correct. OTOH I'm not sure if we fully support required properties — my mental mode of S7 was that you can always construct an object with no arguments, because that's such a useful way to generate a prototype. But it's possible that thinking is centred in vctrs and doesn't make sense in general.

If we do want to support required properties I don't see how to square that with respecting setters. For required properties to work you need to pass the property to the parent and to respect the child setter, you need to pass it to the child. Maybe we have to pass the property to both the child and the parent?

Maybe you could pass the property to both parent and child, but then you'd call the setter twice, which might be confusing (although it does mean that the child definitely can't weaken any checks that the parent does).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the best approach is to pass it to both. This will have a performance impact unfortunately, but hopefully it's minor, and the benefits we get in correctness, enforcement, and simplicity are worth it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to work!

@hadley hadley requested a review from t-kalinowski June 17, 2026 12:50

@t-kalinowski t-kalinowski left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed a few commits that try to make overridden parent properties behave more consistently in default constructors.

The implementation is a little hard to follow because it has to decide separately which arguments belong in the child constructor, which should be forwarded to the parent constructor, and which should be skipped. The tests are probably the clearest description of what I was aiming for here; each added test failed without the corresponding patch.

In short, the tests cover:

  • subclass defaults replacing parent defaults
  • mandatory parent properties being satisfiable from subclass defaults
  • overridden properties still being forwarded through parent ...
  • subclass setters running during construction
  • parent validators rerunning when inherited properties are reset
  • read-only overrides being removed from the constructor without removing unrelated parent args
  • incompatible dynamic settable overrides not being forwarded to the parent

Please take a close look. I’m not certain this is the simplest implementation, but I think the behavior in the tests is worth considering.

https://diffshub.com/RConsortium/S7/compare/c254152d88316466ccf0a5979d316c5309bb05da...b64d8e6548e369c4c0b56383593fd48ba76da9d5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants