-
Notifications
You must be signed in to change notification settings - Fork 42
Description
If a property has a custom setter(), it should be possible to set the property by passing a value to the default constructor. A draft PR, #445, implements this.
However, before #445 can be merged, we need to resolve the question: Should the default constructor call the property setter? There are three possible answers: Always, Never, and Sometimes. Each answer is motivated by a compelling use case.
(Discussion on this question has been organic and spread across different threads, this is my attempt to collate and help us reach a decision)
Never call setters
A compelling use case here is a set-once, read-only thereafter property.
Person <- new_class("Person", properties = list(
birth_name = new_property(
class = class_character,
setter = function(self, value) stop("Cannot set 'birth_name' again")
)
))
person <- Person(birth_name = "John")
try(person@birth_name <- "Bob") # Error: Cannot set 'birth_name' againIn this scenario, new_object() only sets the underlying property attributes using attributes<-, never the property setter. There would then need to be a mechanism for class authors to opt-in to running the setters. This could be via a new_class(initializer=) hook:
Person <- new_class("Person",
initializer = function(self) {
props(self) <- props(self) # opt-in to calling all prop `setter`s
self
})Sometimes call setters
The compelling usage example here is of a deprecated property. If explicitly set by the user, we want the setter to run; otherwise, we don't.
Person <- new_class("Person", properties = list(
first_name = new_property(class = class_character),
firstName = new_property(
getter = function(self) {
warning("@firstName is deprecated; use @first_name instead")
self@first_name
},
setter = function(self, value) {
warning("@firstName is deprecated; use @first_name instead")
self@first_name <- value
self
}
)
))
hadley <- Person(firstName = "Hadley") # warning
hadley@firstName # warning
hadley@firstName <- "John" # warning
hadley <- Person(first_name = "Hadley") # no warningWith this path, new_object() would need to inspect the input value and only conditionally invoke the setter via @<-.
new_object <- function(...) {
props <- list(...)
for (name in names(props)) {
val <- props[[name]]
if (!is.null(val)) # or missing(), or ...
prop(object, name) <- val
}
validate(object)
object
}If is.null() is too strong a check for invoking the setter, we could instead use missing() (and also, make the corresponding constructor formal value quote(expr=)).
For this "deprecated property" use case, we may also want to demote the property from being a named argument in the constructor and instead accept it in .... This would also implicitly make it "missing" if not provided, and never set.
Always call the setters
The compelling use case here is of a property setter that does some more involved initialization, argument coercion, etc.
Person <- new_class("Person", properties = list(
birth_date = new_property(
class = class_Date,
setter = function(self, value) {
self@birthdate <- as.Date(value) # coerce when setting
}
)
))
person <- Person(birthdate = "1999-01-01") # coerces character to DateProperty authors could "opt-out" of calling the setter by including the appropriate checks within setter.
For example, going back to the "set-once" example, the setter would now need to handle initialization too:
Person <- new_class("Person", properties = list(
birth_name = new_property(
class = class_character,
setter = function(self, value) {
if (is.null(self@birth_name)) {
# initializing
self@birth_name <- value
return(self)
}
# not initializing
stop("Cannot set 'birth_name' again")
}
)
))The same would apply to the "Deprecated Property" scenario, but with different constraints.
Care would need to be taken not to accidentally invoke the getter() from the setter(). This could be done either by checking with attr(self, "firstName", TRUE) instead of self@firstName, or by finding another approach, such as accepting a "don't warn" sentinel like NULL in the setter:
Person <- new_class("Person", properties = list(
first_name = new_property(class = class_character),
firstName = new_property(
getter = function(self) {
warning("@firstName is deprecated; use @first_name instead")
self@first_name
},
setter = function(self, value) {
if(!is.null(value)
warning("@firstName is deprecated; use @first_name instead")
self@first_name <- value
self
}
)
))