-
Notifications
You must be signed in to change notification settings - Fork 36
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
Non-generic methods for S7 Objects #436
Comments
Have you already gone through the vignettes? In particular, this and this.
|
yes indeed. That is a generic method though. I would like to define a method that is not generic and cannot be extended |
If this is what you want, R6 might be a better fit for you. Having said that, you can achieve this in S7, too: greet_prop <- new_property(
class_function,
getter = function(self) {
function() message("Hello, ", self@name, "!")
}
)
person <- new_class(
"person",
properties = list(name = class_character, age = class_integer, greet = greet_prop)
)
JohnDoe <- person(name = "John Doe", age = 30L)
JohnDoe@greet() |
Ah! That is interesting! But in this case the property would be a misnamed method. |
S7, like S3 and S4, is built around a generic function OOP system, which differs from encapsulated systems like Rust, R6, or Python. This is very well described by @hadley in Advanced R, https://adv-r.hadley.nz/oo.html#oop-systems, which I'll quote here:
In most simple cases, though, the difference between these two approaches is more about syntax than underlying functionality. For example, in Rust, you can call methods either way: let p = Person {name: "Mary", age: 5};
p.greet(); // Method call via object
Person::greet(p); // Equivalent function call Similarly, in Python: class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def greet(self):
print(f"Hi, I am {self.name}!")
p = Person("Mary", 5)
p.greet() # Object-based method call
Person.greet(p) # Equivalent function call In a generic-function-based system like S7, the sugar differs slightly. The generic function dispatches to a method based on the class of the object, instead of the object carrying around a reference to the method to call. So yes, in S7, a method that dispatches must be associated with a generic function, and you won’t be able to attach methods directly to the class as you would in encapsulated OOP. That said, there are some workarounds. The previous comment by @tdeenes shows how to overload a property: Person <- new_class("Person", properties = list(
name = class_character,
age = class_integer,
greet = new_property(class_function, getter = function(self) {
function() cat(sprintf("Hi, I am %s!\n", self@name))
})
))
p <- Person(name = "Mary", age = 30)
p@greet() # Prints "Hi, I am Mary!" If you're simply trying to create a method that can't be extended (and that takes an instance as the first argument), you could simply define a standalone function. greet <- function(x) {
stopifnot(inherits(x, "Person"))
cat(sprintf("Hi, I am %s!\n", self@name))
} If you really want to mimic the method(`$`, Person) <- function(x, name) {
method <- get(name, mode = "function", envir = parent.frame())
function(...) method(x, ...)
}
p$greet() # Calls greet method This approach gives you some of the encapsulated syntax, but it does come with caveats and isn't idiomatic S7. |
Since this isn't something we're planning on adding to S7 in the near future, I'm going to close this issue, but feel free to keep the discussion going here. |
See also: #202 |
Moving from message-passing to functional OOP benefits from a shift in how we think about software. Typically, OOP is based on passing messages between objects, which are agents that do something in response to a message. Often, a message requires an object to change state, which means it convenient for it to be mutable. In functional OOP, however, generic functions are stateless polymorphic agents (like a REST API), and the objects come to represent only state. Since generics transform state, it is convenient for the state objects to be immutable. Thus, mutability and message-passing tend to be coupled, and the two paradigms become distinct enough to be treated separately. Since much of programming in R involves transformation of state (data), functional OOP is the convention. I would recommend reference classes (from the methods package) or R6 as complements to S7 for the exceptional use cases. |
@lawremi to me, the key selling point of S7 is the type safety it provides as well as multiple dispatch—which S3, R6, and S4 do not provide. It is far less about a functional paradigm. R is a fully featured language and something like S7 could help bring R into the modern era with additional focus on type safety—a la typescript, pydantic (now with type hinting in 3.6), the success of Rust. I think immutability is a great default for end users but as package developers being able to have mutable objects can be very important and useful. Particularly when storing large objects. Cloning these objects frequently can bloat memory usage and puts a larger strain on the GC. I have played with a hybrid approach here if you are at all interested: https://gist.github.com/JosiahParry/86c9ed8417a3eb21a23b977e90947562 |
S4 provides some degree of type safety and multiple dispatch. But neither S4 nor S7 get close to the other examples you mention, which bring typing to the language itself. With respect to scalability, in my experience requirements quickly graduate from reference semantics to more sophisticated distributed computing and out-of-core approaches. This is definitely something we will continue thinking about, though. Thanks for the push. |
I think that encapsulated methods as properties, similar to how @tdeenes showed, is something people will want and use regularly. The mechanics are a little tricky, and I wonder if we should export a helper from S7 to make this easier (perhaps as part of the "convenience props" family in #433). ( new_encapsulated_method <- function(method, name = NULL) {
frmls <- formals(method)
stopifnot("method first argument must be named 'self'" =
identical("self", names(frmls)[1]))
passthroughs <- as_names(names(frmls), named = TRUE)
names(passthroughs)[names(passthroughs) == "..."] <- ""
method_sans_self <- new_function(
args = frmls[-1],
body = as.call(c(method, passthroughs)),
env = baseenv()
)
getter <- function(self) {
environment(method_sans_self) <- environment()
method_sans_self
}
environment(getter) <- list2env(
list(method_sans_self = method_sans_self),
parent = baseenv()
)
new_property(class_function, getter = getter, name = name)
}
environment(new_encapsulated_method) <- asNamespace("S7") Usage example: Thing <- new_class("Thing", properties = list(
time_created = new_property(class_POSIXct, default = quote(Sys.time())),
age = new_encapsulated_method(function(self) {
Sys.time() - self@time_created
}),
id = new_property(class_character, default = quote(sub(
".*0x([0-9a-z]+).*", "\\1", format(environment())
))),
log = new_encapsulated_method(function(self, ..., sep = "") {
cat("[", self@id, "] ", ..., sep = "")
}))
)
thing <- Thing()
thing
#> <Thing>
#> @ time_created: POSIXct[1:1], format: "2024-10-10 08:11:51"
#> @ age : function ()
#> @ id : chr "13ad9e370"
#> @ log : function (..., sep = "")
thing@time_created
#> [1] "2024-10-10 08:11:51 EDT"
thing@age
#> function ()
#> (function (self)
#> {
#> Sys.time() - self@time_created
#> })(self = self)
#> <environment: 0x11a9d8558>
thing@age()
#> Time difference of 0.0230968 secs
thing@id
#> [1] "13ad9e370"
thing@log
#> function (..., sep = "")
#> (function (self, ..., sep = "")
#> {
#> cat("[", self@id, "] ", ..., sep = "")
#> })(self = self, ..., sep = sep)
#> <environment: 0x11a8deb48>
thing@log("hello")
#> [13ad9e370] hello |
Thanks for sketching this out. What use cases do we foresee with this syntax? It seems to me the only advantage is the avoidance of name collisions, at the cost of functional syntax. The syntax might suggest that the objects behave as in the message-passing paradigm, which is not the case (less encapsulation of state, no mutability). |
Indeed, the primary motivation would be avoiding name collisions and aiding discovery. However, a better approach to discovery might be to focus on #435. |
Great point about aiding discovery. That's a nice feature of S7 properties. The S4 slots could never really be that, due to the lack of encapsulation (getters). I guess the pipe operator could help with that? Like |
I am trying to work with S7 in an R package for the first time and am struggling a bit with reconciling S7 with other OOO approaches I've worked with.
Typically, I can define a class and their properties. Then for the class, I can define methods that can be called from the class. I would like to do this with S7 but from what I can tell, it is only possible to create generic methods for S7. And it is not possible to create methods for an S7 class.
What I have been toying with is setting a prop as
class_function
but this allows it to be any function. Not a specific function id like to associate with the class.Is this possible today?
e.g. in rust i might write:
in S7 the equivalent might be something like
but i cannot understand how to create a
greet()
methodThe text was updated successfully, but these errors were encountered: