-
Notifications
You must be signed in to change notification settings - Fork 37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
External S7 classes #341
base: main
Are you sure you want to change the base?
External S7 classes #341
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
To do:
|
@DavisVaughan @t-kalinowski this will need discussion with the full committee before we merge it, but I'd love to get your initial thoughts on both the overall idea and the implementation. |
#Conflicts: # R/zzz.R
if (!is.null(package) && is_class(parent)) { | ||
if (!is.null(parent@package) && !identical(parent@package, package)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it need to be a nested if?
@@ -128,30 +136,44 @@ new_class <- function( | |||
} | |||
|
|||
# Combine properties from parent, overriding as needed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment may be better placed inside inherit_properties()
now
|
||
constructor <- constructor_fun() | ||
if (!is_class(constructor)) { | ||
stop("`constructor_fun()` must yield an S7 class") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be a fairly esoteric error if you don't actually provide constructor_fun
and instead provided package
and name
incorrectly
|
||
constructor <- constructor_fun() | ||
if (!is_class(constructor)) { | ||
stop("`constructor_fun()` must yield an S7 class") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
stop("`constructor_fun()` must yield an S7 class") | |
stop("`constructor_fun()` must return an S7 class") |
#' Classes from other packages | ||
#' | ||
#' @description | ||
#' You need an explicit external class when you want extend a class defined in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#' You need an explicit external class when you want extend a class defined in | |
#' You need an explicit external class when you want to extend a class defined in |
#' when the other package changes your package doesn't need to be rebuilt to | ||
#' get those changes. | ||
#' | ||
#' [new_class()] will automatically convert an S7 class to an external class |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It automatically converts the parent right?
@@ -316,7 +317,7 @@ prop_names <- function(object) { | |||
c("name", "parent", "package", "properties", "abstract", "constructor", "validator") | |||
} else { | |||
class <- S7_class(object) | |||
props <- attr(class, "properties", exact = TRUE) | |||
props <- attr(class, "properties", exact = TRUE)() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth adding class_properties()
or something similar that does this, and maybe adds a comment about the fact that properties are dynamic to support external classes? I think this is called in at least 3 places
@ properties :List of 2 | ||
.. $ x: <S7_property> | ||
.. ..$ name : chr "x" | ||
.. ..$ class : <S7_base_class>: <integer> | ||
.. ..$ getter : NULL | ||
.. ..$ setter : NULL | ||
.. ..$ validator: NULL | ||
.. ..$ default : NULL | ||
.. $ y: <S7_property> | ||
.. ..$ name : chr "y" | ||
.. ..$ class : <S7_base_class>: <integer> | ||
.. ..$ getter : NULL | ||
.. ..$ setter : NULL | ||
.. ..$ validator: NULL | ||
.. ..$ default : NULL | ||
@ properties : function () |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this reduction in info is expected because it is dynamic now?
} | ||
} | ||
) | ||
expect_error(foo2(x = -1), "invalid") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worth a snapshot or no?
} | ||
} | ||
dynamic_args <- function(args, selected) { | ||
missing <- setdiff(selected, names(args)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It took me a few minutes to figure this out, maybe add a comment like
# Pad `args` with currently missing arguments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The motivation for this PR very clear - the current situation is problematic and can easily lead to problems as packages get updated in a user library. The proposed implementation comes w/ some tradeoffs, (most of which can be mitigated w/ more code). It's worth spelling some of these out:
-
Needing to call
class@properties()
as a function is inconsistent w/ how the otherclass@
attributes are accessed. A new user might even call it surprising, (and also, syntactically noisy). Because this is a user-facing interface, I think we should explore alternative implementations that still lets us inject dynamic properties as needed, w/o converting this property to a closure. -
The loss of args names in constructor functions in favor of
...
is, I think, a big loss. It means users lose interactive autocomplete suggestions (w/o substantial work to compensate in the IDE) E.g., i imagine across tidyverse/tidymodels, many constructors will live in external packages. -
Looking at the example in
new_external_class()
raises a few questions (mostly related toprint()
):
> foo <- new_class("foo", properties = list(x = class_integer))
> foo_ex <- new_external_class("S7", "foo", function() foo)
> foo2 <- new_class("foo", parent = foo_ex)
> foo2
<foo> class
@ parent : <S7::foo>
@ constructor: function(...) {...}
@ validator : <NULL>
@ properties :
$ x: <integer>
> foo2()
<foo>
@ x: int(0)
- Should a properties origin be presented here. When I'm working w/ a subclassed class, I typically want to quickly know what is "new" or "different" from the base class, and having that information presented by default
print()
would go a long way. - The
...
signature of the constructor is unfortunate. Would it make sense to include at least the "new" constructor args in the signature? For example, if foo2 had a propertyy
, defined asnew_class("foo", properties(list(y = class_character)), parent = foo_ex)
, then perhaps the constructor signature could be(..., y)
, with...
being forwarded to the parent constructor. - (side questions, not directly related to this PR: why are we presenting an empty class validator property?)
Some stray thoughts:
- Would it make sense to convert
@properties
to a locked and sealed environment, rather than a list? Then the environment could be populated once via a call in.onLoad()
, similar to the way we recommend registering S7 methods. (also, opens the door for active bindings) - Could other work be done in
.onLoad()
? Perhaps, building the default constructor with the proper signature? Or catch and warn about edge cases whenpkgA
must be reinstalled due to an update inpkgB
?
An alternative approach would be to record in the namespace every parent class passed to This is similar to how some C programs check for ABI breakage by comparing struct sizes with those compiled into a library. |
Prior to this PR, if class A in package A extends class B in package B, class A will include a copy of the definition of class B at build-time. Methods are not also copied, so if class B is later updated and package B reinstalled, it's possible to end up with in an inconsistent state where you have the class definition from version 1 and the methods from version 2.
This PR adds a new "external class" object designed to ameliorate this problem by doing more work at run-time rather at build-time. For example, the constructor of class B will now have arguments
...
and it will figure out which arguments go to which constructor at run-time (we prefer to do this at build time in order to generate a constructor with named arguments that can be auto-completed etc).Fixes #317