3

I am trying to turn the @ operator in R into a generic function for the S3 system.

Based on the chapter in Writing R extensions: adding new generic I tried implementing the generic for @ like so:

`@` <- function(object, name) UseMethod("@")
`@.default` <- function(object, name) base::`@`(object, name)

However this doesn't seem to work as it breaks the @ for the S4 methods. I am using Matrix package as an example of S4 instance:

Matrix::Matrix(1:4, nrow=2, ncol=2)@Dim

Error in @.default(Matrix::Matrix(1:4, nrow = 2, ncol = 2), Dim) : no slot of name "name" for this object of class "dgeMatrix"

How to implement a generic @ so it correctly dispatches in the case of S4 classes?


EDIT

Also interested in opinions about why it might not be a good idea?

Karolis Koncevičius
  • 9,417
  • 9
  • 56
  • 89
  • 1
    you could implement `\`@.default\` <- function(object, name) methods:slot(object, name)` but I don't think implementing `@` as a generic is a good idea, and it's not at all convenient -- `\`@\`(Matrix::Matrix(1:4, 2), "Dim")` – Martin Morgan Sep 12 '18 at 14:34
  • 1
    @MartinMorgan Precisely the advice I was going to give, except with `\`@.default\` <- function(object, name) methods::slot(object, deparse(substitute(name)))` so you wouldn't have to quote Dim, e.g. `Matrix::Matrix(1:4, nrow=2, ncol=2)@Dim` works if you use `deparse(substitute())` – duckmayr Sep 12 '18 at 14:37
  • Thanks @duckmayr and @MartinMorgan. I actually tried both `methods::slot` and `substitute` separately, but had no idea it will work in combination. Much appreciated. Also really interested in your opinion about why it wouldn't be a good idea - as currently I see nothing too wrong with using `@` for my custom S3 class (given the generic is implemented correctly to use the base @ for S4 classes). – Karolis Koncevičius Sep 12 '18 at 15:53
  • 1
    @KarolisKoncevičius JDL's answer below cover some of the issues I was worried about. The biggest is that `@` is designed to be for S4 objects, so it could be quite unexpected for it to suddenly be an S3 generic. There is also the secondary issue of a performance hit. On my machine, your generic `@` is 4-5x slower than using `base::\`@\`()`, which is in turn about 50x slower than just using the `@` directly. – duckmayr Sep 19 '18 at 00:26
  • I say secondary in part since we're talking ~100 microseconds vs. ~25 microseconds vs. ~500 nanoseconds, which is not usually meaningful. – duckmayr Sep 19 '18 at 00:35
  • @duckmayr thank you for the comments. Regarding the unexpectedness - it will only be used when the package is loaded of course. And then the `@` generic part will be only visible for one class. If I can make everything work correctly then I am not too worried about this. In addition for S4 `@` is not supposed to be used to by the users anyway I think. Instead each class has setters and getters. – Karolis Koncevičius Sep 19 '18 at 06:09

2 Answers2

3

R's documentation is somewhat confusing as to whether @ is already a generic or not: the help page for @ says it is, but it isn't listed on the internalGenerics page.

The @ operator has specific behaviour as well as (perhaps) being a generic. From the help page for @: "It is checked that object is an S4 object (see isS4), and it is an error to attempt to use @ on any other object." That would appear to rule out writing methods for S3 classes, though the documentation is unclear if this check happens before method dispatch (if there is any) or after (whence it could be skipped if you supplied a specific method for some S3 class).

You can implement what you want by completely redefining what @ is, along the line of the suggestion in comments:

`@.default` <- function(e1,e2) slot(e1,substitute(e2))

but there are two reasons not to do this:

1) As soon as someone loads your package, it supersedes the normal @ function, so if people call it with other S4 objects, they are getting your version rather than the R base version.

2) This version is considerably less efficient than the internal one, and because of (1) you have just forced your users to use it (unless they use the cumbersome construction base::"@"(e1,e2)). Efficiency may not matter to your use case, but it may matter to your users' other code that uses S4.

Practically, a reasonable compromise might be to define your own binary operator %@%, and have the default method call @. That is,

`%@%` <- function(e1,e2) slot(e1,substitute(e2))
setGeneric("%@%")

This is called in practice as follows:

> setClass("testClass",slots=c(a="character")) -> testClass
> x <- testClass(a="cheese")
> x %@% a
[1] "cheese"
JDL
  • 1,496
  • 10
  • 18
  • Thanks for the answer @JDL but it's not quite what I had in mind. I *really* am thinking about overwriting the `@` to work with S3 classes so I can access fields using it like so `myclass@myfield`. Doing this would provide a second operator for value extraction alongside of `$` (which is already generic). – Karolis Koncevičius Sep 15 '18 at 13:07
  • Fair enough — my second paragraph addresses why this probably isn't possible. Defining `%@%` is the closest I can manage, I'm afraid! – JDL Sep 17 '18 at 07:31
  • Well it is possible and already done actually, with the help of answers in the comments. Even managed to add custom-made auto completion for `@`. But I wanted to get opinions about why it is "not a good idea" as @MartinMorgan said in the comments. – Karolis Koncevičius Sep 17 '18 at 07:35
  • An alternative thought: could you make your class S4 instead of S3, and overload the `$` operator rather than the `@` operator? – JDL Sep 17 '18 at 07:36
  • `$` is already generic so people can add their own methods to it with `$.newclass` and define their own autocompletions with `.DollarNames.newclass`. I wanted both of them to work and I want to avoid S4 classes. But appreciate the suggestions! – Karolis Koncevičius Sep 17 '18 at 07:37
  • Fair enough. Have added some more detail into why redefining `@` is a "bad idea". If you don't mind me asking, why don't you want to use S4? – JDL Sep 17 '18 at 07:43
  • but if you think this would be better achieved with S4 I would surely at least attempt it. My main goal is for the object to remain a "matrix". So it can be passed in any function that expects a matrix and work as expected. – Karolis Koncevičius Sep 17 '18 at 07:55
  • That should be possible, if you start with `myClass <- setClass("myClass",contains="matrix",...)`. If you are writing your own function to operate on this class, then you can use `object@.Data` to get at the "matrix part" of the object. – JDL Sep 17 '18 at 08:01
3

In R 4.3.0 and newer, the @ operator will be internally S3 generic, as documented in the latest NEWS:

The @ operator is now an S3 generic. Based on contributions by Tomasz Kalinowski in PR#18482.

You can test on R-devel if you don't want to wait for the release of R 4.3.0 on April 21:

.S3method("@", "zzz", function(object, name) "OK")
structure(0, class = "zzz")@whatever
## [1] "OK"
Mikael Jagan
  • 9,012
  • 2
  • 17
  • 48