I'd like to write a %*% method for a base matrix subclass. My subclass is an S3 class and the documentation of help("%*%") says that %*% is a S4 generic and that S4 methods need to be written for a function of two arguments named x and y. I've written methods for S3 classes for S4 generic methods before using methods::setOldClass() and methods::setMethod() and I've looked at the source code for {Matrix} package for inspiration but for some reason I can't quite get this to work in this case. methods::showMethods() shows my method exists for my target signature but my method never seems to actually get called by R when I try to use it.
x <- diag(3)
class(x) <- c("atm2d", class(matrix()))
print(x)
[,1] [,2] [,3]
[1,] 1 0 0
[2,] 0 1 0
[3,] 0 0 1
attr(,"class")
[1] "atm2d" "matrix" "array"
The default %*% gets rid of my class attribute and I would like to preserve it.
print(x %*% x)
[,1] [,2] [,3]
[1,] 1 0 0
[2,] 0 1 0
[3,] 0 0 1
I've tried to create a %*% method for my class which doesn't get rid of my class attribute:
as.matrix.atm2d <- function(x, ...) {
class(x) <- NULL
x
}
matmult <- function(x, y) {
v <- as.matrix(x) %*% as.matrix(y)
class(v) <- c("atm2d", class(matrix()))
v
}
methods::setOldClass("atm2d")
# this alternate `setOldClass()` also doesn't work
# methods::setOldClass(c("atm2d", class(matrix())))
methods::setMethod("%*%",
c(x = "atm2d", y = "atm2d"),
function(x, y) matmult(x, y))
showMethods() seems to show an S4 method with the expected signature was created:
showMethods("%*%", class = "atm2d")
Function: %*% (package base)
x="atm2d", y="atm2d"
However this method doesn't actually seem to be called by %*%:
print(x %*% x)
[,1] [,2] [,3]
[1,] 1 0 0
[2,] 0 1 0
[3,] 0 0 1
If my method had been called I would have instead expected it to also print out its class:
print(matmult(x, x))
[,1] [,2] [,3]
[1,] 1 0 0
[2,] 0 1 0
[3,] 0 0 1
attr(,"class")
[1] "atm2d" "matrix" "array"
The
%*%operator is internally generic, which means that dispatch happens in C code. Currently (i.e., in R 4.2.3), the corresponding C functiondo_matprod(defined here) contains this check:If neither
xnoryis an S4 object, as in your example, thendo_matprodproceeds to handle them as traditional matrices, not looking at theclassattribute of either argument. The section ofhelp("%*%")to which you refer:tries to express that, but it is not particularly clear. (You did define an S4 method, after all.)
There are two main issues here:
setOldClassallows you to define S4 methods with S3 classes in the signature, but internally generic functions only look for S4 methods when one of the arguments is an S4 object (for speed).%*%is not S3 generic, so even if you registered an S3 method like%*%.zzz, it would not be dispatched.Having said that, the
%*%operator will become S3 generic starting in R 4.3.0, due to be released on April 21. You'll find this entry in the NEWS:When that happens,
%*%will behave like+and other internally generic members of theOpsgroup, in that S3 methods%*%.zzzwill be dispatched where appropriate. However, S4 methods will still not be dispatched when neither argument is an S4 object.Arguably,
setMethodshould be changed to signal a warning or error when you try to define S4 methods that will never be dispatched, as you have in your example.Appendix
It might help to enumerate the types of generic functions and the types of methods that they dispatch, restricting attention to S3 and S4. We'll use this script to define objects for our tests, which should each be run in a new R process:
Non-internally S3 generic functions
These dispatch S3 methods for S3 classes and S4 classes via
UseMethod. When no method is found, they dispatch the default method*.defaultor (if that is not found) throw an error. They never dispatch S4 methods.Non-internally S4 generic functions
These dispatch S4 methods for S3 classes (formally defined with
setOldClass) and S4 classes viastandardGeneric. When no method is found, they dispatch the default method*@default. If the default method is S3 generic, then dispatch happens again, this time to any available S3 methods. However, often the default method just callsstopto signal an error.Internally generic functions
These are all defined in base as primitive functions. You would refer to the help pages or the source code to determine whether they are only S3 generic, only S4 generic, or both S3 and S4 generic. In the third case, S3 dispatch happens only if no suitable S4 methods are found. And, as I've explained already, S4 dispatch happens only if one of the arguments in the signature is an S4 object.
Let's take
+and%*%as examples. Both are S4 generic but only+is S3 generic.Here, S3 methods are dispatched. The first three results are obtained via S3 dispatch. The last result is obtained via S4 dispatch.
Here, S3 methods are not dispatched. The first three results are obtained via the internally defined default method. The last result is obtained via S4 dispatch.