Skip to content
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

S7 with varargs #515

Closed
VisruthSK opened this issue Dec 18, 2024 · 6 comments
Closed

S7 with varargs #515

VisruthSK opened this issue Dec 18, 2024 · 6 comments

Comments

@VisruthSK
Copy link

When converting some rvest S3 classes to S7 I happened upon rvest_field which has an interesting S3 implementation using ...:

rvest_field <- function(type, name, value, attr, ...) {
  structure(
    list(
      type = type,
      name = name,
      value = value,
      attr = attr,
      ...
    ),
    class = "rvest_field"
  )
}

I noted that in the rvest implementation there was only one use case of the dots, and that was to pass a specific value called options so I opted to make that an optional property of the S7 rvest_field, and removed the dots in its construction. That led me to question how one would mimic the original behaviour as closely as possible using S7 though. The first thought I had was to take the dots and stick them into a property:

library(S7)
test <- new_class(
  "test",
  properties = list(
    real_prop = class_any,
    varargs = class_list
  ),
  constructor = function(real_prop, ...) {
    new_object(S7_object(), real_prop = real_prop, varargs = rlang::list2(...))
  }
)

But now you obviously have to do test(10, x=1)@varargs$x which isn't very pretty--and you also lose validation for the faux x "property" which defeats a lot of the benefits of S7 I think (but nominally solves the problem). Dealing with the first issue is easy (see below for a very rough implementation), but I don't know what an nice way of solving the latter would be. Maybe pass a list with arguments and attached properties like
temp(x = "hi", y = 2, list(x = class_character, y = new_property(class_numeric, getter = function(self) self@y, setter = NULL)))
and then parse the list on construction? And then instead of making someone deal with all that everytime they want to create a S7 object with the dots, expose a varargs class which does this and make objects which want to have variable properties inherit from it?

I also don't know how pertinent this is, but I think it would be cool to be able to create S7 objects with variable properties proper, instead of looking up arguments in an list. I'd be happy to work on this if it is useful/feasible.

temp <- new_class(
  "temp",
  properties = list(
    varargs = new_property(
      class_list,
      getter = function(self) self@varargs,
      setter = NULL
    )
  ),
  constructor = function(...) new_object(S7_object(), varargs = rlang::list2(...))
)

`@.temp` <- function(object, name) {
  tryCatch(
    S7:::`@.S7_object`(object, name),
    error = function(e) {
      property <- S7:::`@.S7_object`(object, "varargs")[[name]]
      if (is.null(property)) S7:::`@.S7_object`(object, name) # TODO: throw a better error which talks about instance (i.e. `object`) instead of class (i.e. `class(object)[1]`)
      property
    }
  )
}

t <- temp(x = 1, y = 2)
t@varargs
t@x
try(t@z)
@lawremi
Copy link
Collaborator

lawremi commented Feb 11, 2025

Thanks for raising this; it's an idea worth discussing.

With respect to the rvest use case, while I have never used rvest, it seems reasonable to group non-standard elements into their own list. The code becomes explicit about what is special about the field. Instead of @varargs perhaps @extra would be more meaningful.

Another way to conceptualize this field structure is as a list (just as it was using S3), but one where a subset of its elements are guaranteed to be present (and perhaps of a certain type). That could easily be done by extending class_list and implementing a custom validator. I guess you could use an internal helper class with properties corresponding to those elements in order to implement the validator, but you wouldn't need to expose that to the user.

@VisruthSK
Copy link
Author

Apologies, but do you mind further explaining how you could extend class_list to get this behaviour? I understand the idea of having optional elements in a list (e.g. not checking existence of certain elements in validation), but I'm at a loss at how you would implement this using/in S7. I think that type safety is important, even with variable arguments, and I guess I'm not understanding how you get typed arguments as well as optional arguments in the way you described.

@hadley
Copy link
Member

hadley commented Feb 17, 2025

If you know in advance what the elements will be, you should make them fields. If you don't know what they will be, you should just dump them in a list. In that case, you can't do type checking because you don't know what they are.

@VisruthSK
Copy link
Author

That makes sense, fair enough. Do you think there's any point in automatically handling dots by tweaking new_constructor() to dump them to a prop?

@hadley
Copy link
Member

hadley commented Feb 24, 2025

I don't think so, since it's easy enough to do in a wrapper.

@VisruthSK
Copy link
Author

Fair enough, I'll close the issue then. Thank you!

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

No branches or pull requests

3 participants