RConsortium / S7

S7: a new OO system for R
https://rconsortium.github.io/S7
Other
386 stars 32 forks source link

Allow read-only properties to be set in the constructor #404

Open guslipkin opened 2 months ago

guslipkin commented 2 months ago

Currently the documentation suggests that read-only properties (those with only a getter and no setter) are for computed values such as now or calculating using other properties of the object. However, sometimes you want to create an object with a value that doesn't change, isn't allowed to change, and isn't calculated from other values.

Take this person class, for example. We want to allow the birthdate to be set once and only once on construction, but the person's age will be calculated using the current date.

person <- S7::new_class(
  name = 'person',
  properties = list(
    'name' = S7::class_character,
    'birthdate' = S7::new_property(
      class = S7::class_Date,
      getter = \(self) { self@birthdate }
    ),
    'age' = S7::new_property(
      class = S7::class_any,
      getter = \(self) { Sys.Date() - self@birthdate }
    )
  ),
  constructor = \(name, birthdate) {
    S7::new_object(
      S7::S7_object(),
      'name' = name,
      'birthdate' = birthdate
    )
  }
)

person('Unixtime', as.Date('1970-01-01'))
lawremi commented 2 days ago

I think you could do this by having a setter that throws an error if the Date is already set (not NULL). There's also been the suggestion of a property with a dynamic default, see #376.

guslipkin commented 1 day ago

That's a good idea, but I can't quite get it to work because there are some barriers in the way, including the issue I mentioned in #405 where you can't use default values and a constructor in tandem.

Our setup now looks like this:

person <- S7::new_class(
  name = 'person',
  properties = list(
    'name' = S7::class_character,
    'birthdate' = S7::new_property(
      class = S7::class_Date,
      getter = \(self) { self@birthdate },
      setter = \(self, value) {
        if (is.na(self@birthdate)) self@birthdate <- value else warning('Cannot set read-only value')
        return(self)
      },
    ),
    'age' = S7::new_property(
      class = S7::class_any,
      getter = \(self) { Sys.Date() - self@birthdate }
    )
  ),
  constructor = \(name, birthdate = as.Date(NA)) {
    S7::new_object(
      S7::S7_object(),
      'name' = name,
      'birthdate' = birthdate
    )
  }
)

person('Unixtime')

If you run this, you'll get a C stack error. I'm not really sure why this is.