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' again
In 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 warning
With 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 Date
Property 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
}
)
))
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
settersA compelling use case here is a set-once, read-only thereafter property.
In this scenario,
new_object()only sets the underlying property attributes usingattributes<-, never the propertysetter. There would then need to be a mechanism for class authors to opt-in to running the setters. This could be via anew_class(initializer=)hook:Sometimes call
settersThe 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.
With this path,
new_object()would need to inspect the input value and only conditionally invoke the setter via@<-.If
is.null()is too strong a check for invoking the setter, we could instead usemissing()(and also, make the corresponding constructor formal valuequote(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
settersThe compelling use case here is of a property setter that does some more involved initialization, argument coercion, etc.
Property 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
setterwould now need to handle initialization too: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 thesetter(). This could be done either by checking withattr(self, "firstName", TRUE)instead ofself@firstName, or by finding another approach, such as accepting a "don't warn" sentinel likeNULLin the setter: