|
1 | | -When an S7 class extends an S4 class, `new_class()` automatically registers the |
2 | | -new class with S4. After that, an S4 generic with a method for `Parent` will |
3 | | -dispatch on a `Child` instance: |
| 1 | +`new_class()` can use an S4 class as its parent. The S4 class can be supplied |
| 2 | +as a class definition, such as `methods::getClass("Parent")`, or as a class |
| 3 | +generator returned by `methods::setClass()`. The S4 parent slots become S7 |
| 4 | +properties on the new S7 class. |
| 5 | + |
| 6 | +When an S7 class extends an S4 class, `new_class()` automatically registers an |
| 7 | +S4 old class for the S7 class. This lets S4 generics dispatch through the S4 |
| 8 | +parent: |
4 | 9 |
|
5 | 10 | ```r |
6 | | -setMethod("g", "Parent", function(x) ...) |
7 | | -g(child) # uses Parent method if no more specific Child method exists |
8 | | -``` |
| 11 | +methods::setClass("Parent", slots = list(x = "numeric")) |
| 12 | +Child <- new_class("Child", methods::getClass("Parent"), |
| 13 | + properties = list(y = class_character) |
| 14 | +) |
9 | 15 |
|
10 | | -But the method receives the original S7 object, not a formal S4 instance, so |
11 | | -this approach is a compatibility bridge, not full substitutability. |
| 16 | +methods::is(Child(x = 1, y = "a"), "Parent") |
| 17 | +``` |
12 | 18 |
|
13 | 19 | Things that work reasonably: |
14 | 20 |
|
| 21 | +* S4 dispatch inherits through the S4 parent. An S4 generic with a method for |
| 22 | + `Parent` can dispatch on a `Child` object. |
15 | 23 | * `methods::is(child, "Parent")` returns `TRUE`. |
16 | | -* S4 dispatch inherits through `Parent`. |
17 | | -* `methods::validObject(child)` validates the S4 parent slots through S4 and |
18 | | - recursively validates S7 properties and class validators through S7. |
19 | | -* `methods::slotNames(child)` reports the S4 parent slots. S7 extension |
20 | | - properties are not registered as formal S4 slots. |
21 | | -* `methods::slot(child, "x")` works for stored attributes, but bypasses the S7 |
22 | | - property layer. |
23 | | -* `child@x` works through S7 property access. With the current oldClass |
24 | | - representation, the object is not S4-bit so `@` can dispatch to S7's |
25 | | - `@.S7_object` method. If S7 objects that extend S4 classes are represented |
26 | | - as S4-bit objects in the future, R's `@` primitive will need to dispatch |
27 | | - before taking the S4 slot-access path. |
28 | | -* `methods::initialize(child, ...)` works for reinitializing an existing S7 |
29 | | - object through S4 code. This is most useful for inherited S4 methods that |
30 | | - update parent slots or data parts. Values are routed through S7 property |
31 | | - assignment, so S7 metadata and validation are preserved. |
32 | | -* `as(child, "Parent")` can produce a real S4 `Parent` object containing the |
33 | | - parent slots. |
34 | | - |
35 | | -Likely brittle or incompatible with S4-method assumptions: |
36 | | - |
37 | | -* `isS4(child)` is `FALSE` with the current oldClass representation. This |
38 | | - avoids advertising formal S4 object invariants that S7 objects do not |
39 | | - satisfy. S4 code is still exposed to S3-compatible, non-scalar `class()` |
40 | | - vectors through the oldClass bridge. |
41 | | -* `class(child)` is length greater than 1, for example |
42 | | - `c("Child", "S4/Parent", "S7_object")`. This can break S4 code that treats |
43 | | - `class()` as a scalar class name. Code that needs a scalar primary class |
44 | | - name should use `class(x)[1L]`, or `methods::class1(x)` once that helper |
45 | | - has been made public. |
46 | | -* Methods that access slots with `methods::slot()` and `methods::slot<-()` |
47 | | - bypass the S7 property layer. This is similar to deriving from an S3 class |
48 | | - whose methods use `attr()` and `attr<-()` directly. Overriding S4 slots is |
49 | | - therefore discouraged. |
50 | | -* Calls like `methods::new(class(x)[1L], ...)` or |
51 | | - `methods::new(class1(x), ...)` construct a new S4 object from S4's class |
52 | | - definition. For an S7 class registered with S4, the result can satisfy |
53 | | - `inherits(object, "S7_object")` through the S4 oldClass graph while still |
54 | | - lacking the `S7_class` attribute. Such objects are not S7 instances, and |
55 | | - `methods::validObject()` will reject them. Code that is updating an |
56 | | - existing object should prefer `methods::initialize(x, ...)`, which gives |
57 | | - S7's S4 `initialize()` method a chance to preserve S7 metadata. |
58 | | - |
59 | | -In summary, inherited S4 methods must be written against the "registered |
60 | | -oldClass that preserves the S4 parent" contract, not the stricter "formal S4 |
61 | | -instance" contract. They should use a scalar primary-class helper where |
62 | | -appropriate and avoid `slot()` and `slot<-()` when they need S7 property |
63 | | -management to apply. |
| 24 | +* S4 parent slots are available as S7 properties, so `prop(child, "x")` and |
| 25 | + `child@x` use the S7 property path. |
| 26 | +* `methods::slot(child, "x")` can read stored S4 parent slots and stored S7 |
| 27 | + properties. Direct slot access bypasses custom S7 property behavior, so S4 |
| 28 | + code should use it only for slots it owns. |
| 29 | +* `methods::validObject(child)` checks S4 slot types and recursively runs S7 |
| 30 | + property validation and S7 class validators. |
| 31 | +* `methods::initialize(child, ...)` can reinitialize S7 objects from S4 code. |
| 32 | + Unnamed S7, S4, and S3-data-part arguments contribute their known |
| 33 | + properties or slots; later arguments override earlier arguments, and named |
| 34 | + arguments override unnamed ones. |
| 35 | +* `methods::as(child, "Parent")` and `convert(child, to = Parent)` can produce |
| 36 | + a real S4 object for the S4 parent class. |
| 37 | + |
| 38 | +Caveats: |
| 39 | + |
| 40 | +* An S7 object that directly extends S4 is currently represented as an S3 |
| 41 | + old-class object, not as an S4-bit object, so `isS4(child)` is `FALSE`. |
| 42 | + This avoids advertising full S4 object invariants that the object does not |
| 43 | + satisfy. |
| 44 | +* The S3 class vector is not scalar. S4 code that needs one primary class name |
| 45 | + should use `class(x)[1L]`, or `methods::class1(x)` if available. |
| 46 | +* `methods::new(class(x)[1L], ...)` creates a fresh S4 object from the S4 class |
| 47 | + definition. For an S7 class registered with S4, that object can inherit from |
| 48 | + `S7_object` through the old-class graph while lacking the `S7_class` slot or |
| 49 | + attribute that makes it an S7 object. Code that updates an existing object |
| 50 | + should prefer `methods::initialize(x, ...)`. |
| 51 | +* S4 methods that call `methods::slot<-()` can bypass S7 property setters and |
| 52 | + ordinary S7 validation. Call `methods::validObject()` after such updates when |
| 53 | + the object should satisfy the S7 class contract. |
| 54 | +* Properties with custom getters or setters cannot be exposed as S4 slots. An |
| 55 | + S7 class that extends S4, or that is intended to be extended by S4, must use |
| 56 | + stored properties for the properties that need to cross the S4 boundary. |
| 57 | +* `NULL` properties are stored internally using the same sentinel that S4 uses |
| 58 | + for `NULL` slots. `prop()`, `@`, and `methods::slot()` translate this back to |
| 59 | + `NULL`; code that reads raw attributes should treat the representation as an |
| 60 | + implementation detail. |
| 61 | + |
| 62 | +S4 classes can also extend S7 classes with `S4_register_contains()`. See |
| 63 | +`S4_register()` for details. |
0 commit comments