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

Support in place property updating? #525

Open
ltuijnder opened this issue Feb 3, 2025 · 1 comment
Open

Support in place property updating? #525

ltuijnder opened this issue Feb 3, 2025 · 1 comment

Comments

@ltuijnder
Copy link

ltuijnder commented Feb 3, 2025

Would it be possible to support in place updates of properties like R6?

Eg. that with a slight adjustment of the class definition the object would behave as follows:

T <- new_class("T", properties = list(a = class_numeric))
update <- \(obj) {
  obj@a <- obj@a + 1
  return(invisible(NULL))
}
t <- T(a = 1)
update(t)
t@a # -> 2 (it now returns 1)

Don't get me wrong, I think by default the above behavior is correct. But I would somehow slightly adapt my class definition such that the above is possible.

In R only environments provide in-place updates so I tried building a solution around that.

A naive approach would be define a class with a class_environment as parent. But this is not possible duo: #290

So a work around is to manually manage the environment within the S7 object and have getters and setters update that.

T <- new_class(
  "T",
  properties = list(
    .storage = new_property(
      class = class_environment,
      setter = \(self, value) {
        if (is.null(self@.storage)) {
          self@.storage <- value
          return(self)
        }
        stop("Cannot set @.storage after creation")
      }
    ),
    a = new_property(
      class = class_numeric,
      setter = \(self, value) {
        stopifnot("@a should be a numeric" = is.numeric(value))
        assign('a', value, self@.storage)
        self
      },
      getter = \(self) get('a', self@.storage),
      validator = \(value) print("I never get called! Even not with `S7::validate()`")
    )
  ),
  constructor = \(a = numeric(0L)) {
    storage <- new.env(parent = emptyenv())
    new_object(S7_object(), .storage = storage, a = a)
  }
)

The problems with the approach

This works but as the example highlights it has a couple of problems:

  1. The type of my object is not checked anymore duo: Type checking for dynamic properties #450 . So I have to manually check it in the setter.
    • Ideally my code should use the same type checking as is done internally by S7.
    • The error raised does not nicely gather with other validation errors and is formatted differently.
  2. Custom validators do not work anymore because we are defining a dynamic property. So this also needs to be done at the setter property.
  3. It is quite verbose to define a class with the desired behavior with what feels like a lot of boilerplate code. Making my code less readable and comprehensive to understand.

Work arounds:

The current solutions I have at my disposals are:

  1. Live with the above downsides: But manually type checking feels like a job for S7 and does not interact anymore with S7::validate(). It also make my code less readable.
  2. Just directly work with environments: But this defeats the purpose of using S7 in the first place.. I want to be able to structurally define my classes and have them automatically validated.
  3. Use R6: but then I have to mix 2 OOP systems in one code base.. Making my code base more complex + I do not have the automatic type validation. Also I am personally not the biggest fan of encapsulated OOP and would rather base my solution on S7.

Why do I care?

When developing Applications (like shiny apps). You have a long running process and usually objects are created once on initialization of the app (or session) and various callbacks drive the behavior of your server (in shiny these are observes / reactive and renders).

Objects that provide in place updates are perfect to managing state in an app.

I know that in shiny you can manage state with reactiveVal(ues) but:

  • in my case I do not care about further down process triggering so it feels like unnecessary overhead.
  • reactivesValues must be consumed within a reactive context which is not the case if I want to manage state outside of my sessions. I then have to spam isolates(). In my case I have global "App" object that lives on the process level not session level.
  • Also with reactiveValues I do not have the nice automatic type validation and class structure that S7 provides.
  • There are plenty of other application servers out there that are not shiny. And not supporting this is holding back maybe a new server framework is based on S7.

Sorry for the long post, but it would be cool if something like this can get supported or at least address the outstanding issues with the current approach.

@hadley
Copy link
Member

hadley commented Feb 3, 2025

I think the place to start would be to resolve #290 so that you can inherit from environments. But this will require careful analysis to ensure that we don't reintroduce any subtle bugs.

(But I do wonder if mutable S7 objects are going to be generally confusing; IMO there's something nice about the connection between mutability and $ methods found in R6).

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

2 participants