Skip to content

Proposal: allow S7 class inheritance merging and a "S3" nextMethod() dispatch approach.#566

Open
jtlandis wants to merge 2 commits intoRConsortium:mainfrom
jtlandis:main
Open

Proposal: allow S7 class inheritance merging and a "S3" nextMethod() dispatch approach.#566
jtlandis wants to merge 2 commits intoRConsortium:mainfrom
jtlandis:main

Conversation

@jtlandis
Copy link
Contributor

This PR provides a way for S7 objects to dynamically inherit from the same root class. For example, if one package “A” exports a popular S7 class, and is then extended directly by package “B” and “C”, this is currently possible. Specifically, B can be made from C, and C can be made from B (because they both are instances of A), but this causes them to loose all information relating to the original class.

For example, creating an instance of C through B will cause the resulting inheritance to look like <C/A> with all properties relating to B being present as attributes, but not considered to be anything related to the class inheritance.

This PR would make it so creating such instances, i.e. C from B, would result in an inheritance of <C/B/A> in which properties of B are preserved, provided that they do not interfere with properites defined in the explicit class C.

Change 1

The main change is a single function merge_S7_class(class, other), which does a few things:

  1. ensures that other is an instance of class’s parent, and can thus be interchangable with the original parent.
  2. creates a new list of properites for the computed parent, which is the union of properites from the other parent and those specified with class.
  3. sets the parent of class to be other

With these changes, it is enough to create the intended behavior.

example of dynamic (or S3 like) inheritance

library(S7)
class_a <- new_class(
  "a",
  properties = list(
    a = class_character
  )
)
class_b <- new_class(
  "b",
  parent = class_a,
  properties = list(
    b = class_numeric
  ),
  constructor = function(a_obj = class_a(), b = character()) {
    new_object(a_obj, b = b)
  }
)
class_d <- new_class(
  "d",
  parent = class_b,
  properties = list(
    d = class_any
  ),
  constructor = function(b_obj = class_b(), d = NULL) {
    new_object(b_obj, d = d)
  }
)
class_c <- new_class(
  "c",
  parent = class_a,
  properties = list(
    c = class_logical
  ),
  constructor = function(a_obj = class_a(), c = logical()) {
    new_object(a_obj, c = c)
  }
)
class_e <- new_class(
  "e",
  parent = class_c,
  properties = list(
    e = class_any
  ),
  constructor = function(c_obj = class_b(), e = NULL) {
    new_object(c_obj, e = e)
  }
)
aa <- class_a(a = "hello")
ba <- class_b(aa, b = 1)
dba <- class_d(ba)

# dba inherits from <a>, thus we can now
# create <c> from <d/b/a>
cdba <- class_c(dba, c = TRUE)

class(cdba)
#> [1] "c"         "d"         "b"         "a"         "S7_object"

# This object is still consider an instance of <d>
# even though class_c doesnt directly inherit from <d>
S7_inherits(cdba, class_a)
#> [1] TRUE
S7_inherits(cdba, class_d)
#> [1] TRUE
S7_inherits(cdba, class_c)
#> [1] TRUE

# NOTE: with this change, and possibly controversally,
# the parent of cdba, and class_c are NOT identical
identical(S7_class(cdba), class_c)
#> [1] FALSE
attr(S7_class(cdba), "parent")
#> <d> class
#> @ parent     : <b>
#> @ constructor: function(b_obj, d) {...}
#> @ validator  : <NULL>
#> @ properties :
#>  $ a: <character>          
#>  $ b: <integer> or <double>
#>  $ d: <ANY>
attr(class_c, "parent")
#> <a> class
#> @ parent     : <S7_object>
#> @ constructor: function(a) {...}
#> @ validator  : <NULL>
#> @ properties :
#>  $ a: <character>

# we can access the properties associated with <d> or <b>
cdba@b
#> [1] 1
cdba@d
#> NULL

# an error occurs if we cannot merge S7 classes
edba <- class_e(dba)
#> Error in merge_S7_class(class, S7_class(.parent)): The class <e> cannot be merged with other class <d> because <c> is not in the other's heirarchy

# so long as the input object has the appropriate class in the
# inheritiance, it can be merged
ecdba <- class_e(cdba)
ecdba
#> <e>
#>  @ a: chr "hello"
#>  @ c: logi TRUE
#>  @ e: NULL
#>  @ b: num 1
#>  @ d: NULL

Change 2

The second change is the function next_super(). This is to support method dispatch when the methods between our class C and some super method A is unknown. This can thus allow packages to use a generic across the entire parent class heirarchy.

Essentually, works the same as super except we find the appropriate parent class from the object, as this may be the source of truth. Then ignore the first class from class_dispatch(), as the goal is to user the next available method. The below example shows its usecase.

log_class <- function(x) {
  cat("inherits: ", x, "\n")
}
bar <- new_generic("bar", "x")
method(bar, class_a) <- function(x) {
  log_class("a")
}
method(bar, class_b) <- function(x) {
  bar(super(x, class_a))
  log_class("b")
}
method(bar, class_d) <- function(x) {
  bar(super(x, class_b))
  log_class("d")
}
method(bar, class_c) <- function(x) {
  bar(super(x, class_a))
  log_class("c")
}
method(bar, class_e) <- function(x) {
  bar(super(x, class_c))
  log_class("e")
}
baz <- new_generic("baz", "x")
method(baz, class_a) <- function(x) {
  log_class("a")
}
method(baz, class_b) <- function(x) {
  baz(next_super(x, class_b))
  log_class("b")
}
method(baz, class_d) <- function(x) {
  baz(next_super(x, class_d))
  log_class("d")
}
method(baz, class_c) <- function(x) {
  baz(next_super(x, class_c))
  log_class("c")
}
method(baz, class_e) <- function(x) {
  baz(next_super(x, class_e))
  log_class("e")
}
# bar uses  `super`, disptaching to explicit methods
bar(ecdba)
#> inherits:  a 
#> inherits:  c 
#> inherits:  e
# bar uses `next_super`, dispatching on the next inherited superclasses
baz(ecdba)
#> inherits:  a 
#> inherits:  b 
#> inherits:  d 
#> inherits:  c 
#> inherits:  e
# we will get an error if there is no next method
baz(next_super(ecdba, class_a))
#> Error: Can't find method for `baz(S3<S7_super>)`.

Created on 2025-09-13 with reprex v2.1.1

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant